diff --git a/agents/nemo-studio-copilot-spec/AGENT-SPEC.md b/agents/nemo-studio-assistant-spec/AGENT-SPEC.md similarity index 91% rename from agents/nemo-studio-copilot-spec/AGENT-SPEC.md rename to agents/nemo-studio-assistant-spec/AGENT-SPEC.md index 128371a178..43db1adc16 100644 --- a/agents/nemo-studio-copilot-spec/AGENT-SPEC.md +++ b/agents/nemo-studio-assistant-spec/AGENT-SPEC.md @@ -1,13 +1,13 @@ --- -name: nemo-studio-copilot +name: nemo-studio-assistant created_timestamp: 2026-07-28T20:09:49Z author: Danielle Ali and Codex --- -# Agent Spec: nemo-studio-copilot +# Agent Spec: nemo-studio-assistant > This file is the durable contract for the local NeMo Platform assistant. -> Keep it aligned with the implementation under `agents/nemo-studio-copilot/`. +> Keep it aligned with the implementation under `agents/nemo-studio-assistant/`. > The adjacent `agent.yaml` and `skills/` directory are the clean, size-bounded > fileset used for Platform registration; the source project retains its > packaging config and MCP executable. @@ -82,15 +82,15 @@ The mission is grounded in the current deployment proof of concept and its imple ## Evaluation Setup -Unit coverage lives in `agents/nemo-studio-copilot/tests/test_nemo_studio_copilot.py`. Run it with: +Unit coverage lives in `agents/nemo-studio-assistant/tests/test_nemo_studio_assistant.py`. Run it with: ```bash -uv run --frozen pytest agents/nemo-studio-copilot/tests/test_nemo_studio_copilot.py -v +uv run --frozen pytest agents/nemo-studio-assistant/tests/test_nemo_studio_assistant.py -v ``` -The prior NAT evaluation YAML was removed because it depended on NAT's custom workflow and `/generate/full` endpoint. Each retained case in `nemo-studio-copilot-eval-data.json` now has deterministic unit coverage for its required `nemo_api` tool path, collected by the root CI test suite. +The prior NAT evaluation YAML was removed because it depended on NAT's custom workflow and `/generate/full` endpoint. Each retained case in `nemo-studio-assistant-eval-data.json` now has deterministic unit coverage for its required `nemo_api` tool path, collected by the root CI test suite. -Manual Studio validation is documented in `agents/nemo-studio-copilot/tests/smoke_test.md`. Current coverage gaps include fileset listing, missing-workspace clarification, fast-path failure containment, destructive-action ambiguity, iteration limits, retry limits, cancellation, and end-to-end latency thresholds. +Manual Studio validation is documented in `agents/nemo-studio-assistant/tests/smoke_test.md`. Current coverage gaps include fileset listing, missing-workspace clarification, fast-path failure containment, destructive-action ambiguity, iteration limits, retry limits, cancellation, and end-to-end latency thresholds. ## Change Scope diff --git a/agents/nemo-studio-copilot-spec/agent.yaml b/agents/nemo-studio-assistant-spec/agent.yaml similarity index 89% rename from agents/nemo-studio-copilot-spec/agent.yaml rename to agents/nemo-studio-assistant-spec/agent.yaml index 59ec95248d..f88b8ea792 100644 --- a/agents/nemo-studio-copilot-spec/agent.yaml +++ b/agents/nemo-studio-assistant-spec/agent.yaml @@ -1,6 +1,6 @@ config_format: nemo-agents-spec-v1 -name: nemo-studio-copilot -description: NeMo Studio copilot for discovering, configuring, operating, evaluating, and troubleshooting NeMo Platform resources through approval-aware SDK actions. +name: nemo-studio-assistant +description: NeMo Studio assistant for discovering, configuring, operating, evaluating, and troubleshooting NeMo Platform resources through approval-aware SDK actions. instructions: system: @@ -56,7 +56,7 @@ mcp: servers: nemo_studio: transport: stdio - url: env NMP_BASE_URL=$NMP_BASE_URL NMP_WORKSPACE=$NMP_WORKSPACE nemo-studio-copilot-mcp + url: env NMP_BASE_URL=$NMP_BASE_URL NMP_WORKSPACE=$NMP_WORKSPACE nemo-studio-assistant-mcp exposure: harness_native tools: @@ -70,4 +70,4 @@ telemetry: enabled: false provider: relay output_dir: ./artifacts/relay - project: nemo-studio-copilot + project: nemo-studio-assistant diff --git a/agents/nemo-studio-copilot-spec/skills/auditor/SKILL.md b/agents/nemo-studio-assistant-spec/skills/auditor/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot-spec/skills/auditor/SKILL.md rename to agents/nemo-studio-assistant-spec/skills/auditor/SKILL.md diff --git a/agents/nemo-studio-copilot-spec/skills/benchmark-execution/SKILL.md b/agents/nemo-studio-assistant-spec/skills/benchmark-execution/SKILL.md similarity index 98% rename from agents/nemo-studio-copilot-spec/skills/benchmark-execution/SKILL.md rename to agents/nemo-studio-assistant-spec/skills/benchmark-execution/SKILL.md index d6b67d81aa..14a7027fce 100644 --- a/agents/nemo-studio-copilot-spec/skills/benchmark-execution/SKILL.md +++ b/agents/nemo-studio-assistant-spec/skills/benchmark-execution/SKILL.md @@ -4,7 +4,7 @@ description: "Benchmark task execution contract: complete every numbered require --- # Benchmark execution contract -This skill defines the execution requirements that every nemo-studio-copilot run under +This skill defines the execution requirements that every nemo-studio-assistant run under `tests/agentic-use/` must satisfy so the canonical gate (`tests/agentic-use/passrate_token_policy_gate.py`) can score the run on verifier pass-rate and token totals. See diff --git a/agents/nemo-studio-copilot-spec/skills/entities/SKILL.md b/agents/nemo-studio-assistant-spec/skills/entities/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot-spec/skills/entities/SKILL.md rename to agents/nemo-studio-assistant-spec/skills/entities/SKILL.md diff --git a/agents/nemo-studio-copilot-spec/skills/evaluator/SKILL.md b/agents/nemo-studio-assistant-spec/skills/evaluator/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot-spec/skills/evaluator/SKILL.md rename to agents/nemo-studio-assistant-spec/skills/evaluator/SKILL.md diff --git a/agents/nemo-studio-copilot-spec/skills/files/SKILL.md b/agents/nemo-studio-assistant-spec/skills/files/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot-spec/skills/files/SKILL.md rename to agents/nemo-studio-assistant-spec/skills/files/SKILL.md diff --git a/agents/nemo-studio-copilot-spec/skills/guardrails/SKILL.md b/agents/nemo-studio-assistant-spec/skills/guardrails/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot-spec/skills/guardrails/SKILL.md rename to agents/nemo-studio-assistant-spec/skills/guardrails/SKILL.md diff --git a/agents/nemo-studio-copilot-spec/skills/inference/SKILL.md b/agents/nemo-studio-assistant-spec/skills/inference/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot-spec/skills/inference/SKILL.md rename to agents/nemo-studio-assistant-spec/skills/inference/SKILL.md diff --git a/agents/nemo-studio-copilot-spec/skills/secrets/SKILL.md b/agents/nemo-studio-assistant-spec/skills/secrets/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot-spec/skills/secrets/SKILL.md rename to agents/nemo-studio-assistant-spec/skills/secrets/SKILL.md diff --git a/agents/nemo-studio-copilot-spec/skills/workspace/SKILL.md b/agents/nemo-studio-assistant-spec/skills/workspace/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot-spec/skills/workspace/SKILL.md rename to agents/nemo-studio-assistant-spec/skills/workspace/SKILL.md diff --git a/agents/nemo-studio-copilot/.dockerignore b/agents/nemo-studio-assistant/.dockerignore similarity index 78% rename from agents/nemo-studio-copilot/.dockerignore rename to agents/nemo-studio-assistant/.dockerignore index 9ffcb5cd78..de5a556e5b 100644 --- a/agents/nemo-studio-copilot/.dockerignore +++ b/agents/nemo-studio-assistant/.dockerignore @@ -16,5 +16,3 @@ dist/ build/ .venv/ node_modules/ -tests/ -src/nemo_studio_copilot/nemo-studio-copilot-eval-data.json diff --git a/agents/nemo-studio-copilot/Dockerfile.fabric-local b/agents/nemo-studio-assistant/Dockerfile.fabric-local similarity index 95% rename from agents/nemo-studio-copilot/Dockerfile.fabric-local rename to agents/nemo-studio-assistant/Dockerfile.fabric-local index da8fa17ba9..97a1f2fb5a 100644 --- a/agents/nemo-studio-copilot/Dockerfile.fabric-local +++ b/agents/nemo-studio-assistant/Dockerfile.fabric-local @@ -52,11 +52,11 @@ RUN --mount=type=cache,id=uv_cache,target=/root/.cache/uv,sharing=locked \ chmod -R a+rX /opt/uv /workspace/.venv -LABEL org.opencontainers.image.title="nemo-studio-copilot" \ +LABEL org.opencontainers.image.title="nemo-studio-assistant" \ org.opencontainers.image.version="0.1.0" \ org.opencontainers.image.authors="Danielle Ali" \ org.opencontainers.image.created="2026-08-05T19:39:51Z" \ - org.opencontainers.image.description="NeMo Studio Copilot — a Fabric Deep Agents assistant for NeMo Platform tasks." \ + org.opencontainers.image.description="NeMo Studio Assistant — a Fabric Deep Agents assistant for NeMo Platform tasks." \ org.opencontainers.image.revision="ca85d0febce9ced44a8d7dfbb2e3391d829697f9" \ org.opencontainers.image.source="https://github.com/NVIDIA-NeMo/nemo-platform.git" \ com.nemo.agent.id="c9acc9e1c696" \ diff --git a/agents/nemo-studio-copilot/agent.yaml b/agents/nemo-studio-assistant/agent.yaml similarity index 73% rename from agents/nemo-studio-copilot/agent.yaml rename to agents/nemo-studio-assistant/agent.yaml index ab515b22fd..b820107bb6 100644 --- a/agents/nemo-studio-copilot/agent.yaml +++ b/agents/nemo-studio-assistant/agent.yaml @@ -1,6 +1,6 @@ config_format: nemo-agents-spec-v1 -name: nemo-studio-copilot -description: NeMo Studio copilot for discovering, configuring, operating, evaluating, and troubleshooting NeMo Platform resources through approval-aware SDK actions. +name: nemo-studio-assistant +description: NeMo Studio assistant for discovering, configuring, operating, evaluating, and troubleshooting NeMo Platform resources through approval-aware SDK actions. instructions: system: @@ -42,21 +42,21 @@ models: skills: paths: - - src/nemo_studio_copilot/skills/auditor - - src/nemo_studio_copilot/skills/benchmark-execution - - src/nemo_studio_copilot/skills/entities - - src/nemo_studio_copilot/skills/evaluator - - src/nemo_studio_copilot/skills/files - - src/nemo_studio_copilot/skills/guardrails - - src/nemo_studio_copilot/skills/inference - - src/nemo_studio_copilot/skills/secrets - - src/nemo_studio_copilot/skills/workspace + - src/nemo_studio_assistant/skills/auditor + - src/nemo_studio_assistant/skills/benchmark-execution + - src/nemo_studio_assistant/skills/entities + - src/nemo_studio_assistant/skills/evaluator + - src/nemo_studio_assistant/skills/files + - src/nemo_studio_assistant/skills/guardrails + - src/nemo_studio_assistant/skills/inference + - src/nemo_studio_assistant/skills/secrets + - src/nemo_studio_assistant/skills/workspace mcp: servers: nemo_studio: transport: stdio - url: env NMP_BASE_URL=$NMP_BASE_URL NMP_WORKSPACE=$NMP_WORKSPACE nemo-studio-copilot-mcp + url: env NMP_BASE_URL=$NMP_BASE_URL NMP_WORKSPACE=$NMP_WORKSPACE nemo-studio-assistant-mcp exposure: harness_native tools: @@ -70,4 +70,4 @@ telemetry: enabled: false provider: relay output_dir: ./artifacts/relay - project: nemo-studio-copilot + project: nemo-studio-assistant diff --git a/agents/nemo-studio-copilot/constraints.fabric-local.txt b/agents/nemo-studio-assistant/constraints.fabric-local.txt similarity index 100% rename from agents/nemo-studio-copilot/constraints.fabric-local.txt rename to agents/nemo-studio-assistant/constraints.fabric-local.txt diff --git a/agents/nemo-studio-copilot/pyproject.toml b/agents/nemo-studio-assistant/pyproject.toml similarity index 70% rename from agents/nemo-studio-copilot/pyproject.toml rename to agents/nemo-studio-assistant/pyproject.toml index b4eca576f7..480377fcee 100644 --- a/agents/nemo-studio-copilot/pyproject.toml +++ b/agents/nemo-studio-assistant/pyproject.toml @@ -6,9 +6,9 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "nemo-studio-copilot" +name = "nemo-studio-assistant" version = "0.1.0" -description = "NeMo Studio Copilot — a Fabric Deep Agents assistant for NeMo Platform tasks." +description = "NeMo Studio Assistant — a Fabric Deep Agents assistant for NeMo Platform tasks." requires-python = ">=3.12,<3.14" dependencies = [ "nemo-platform", @@ -17,10 +17,10 @@ dependencies = [ ] [project.scripts] -nemo-studio-copilot-mcp = "nemo_studio_copilot.mcp_server:main" +nemo-studio-assistant-mcp = "nemo_studio_assistant.mcp_server:main" [tool.hatch.build.targets.wheel] -packages = ["src/nemo_studio_copilot"] +packages = ["src/nemo_studio_assistant"] [tool.uv.sources] nemo-agents-plugin = { path = "../../plugins/nemo-agents", editable = true } diff --git a/agents/nemo-studio-copilot/skills/auditor/SKILL.md b/agents/nemo-studio-assistant/skills/auditor/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot/skills/auditor/SKILL.md rename to agents/nemo-studio-assistant/skills/auditor/SKILL.md diff --git a/agents/nemo-studio-copilot/skills/benchmark-execution/SKILL.md b/agents/nemo-studio-assistant/skills/benchmark-execution/SKILL.md similarity index 98% rename from agents/nemo-studio-copilot/skills/benchmark-execution/SKILL.md rename to agents/nemo-studio-assistant/skills/benchmark-execution/SKILL.md index d6b67d81aa..14a7027fce 100644 --- a/agents/nemo-studio-copilot/skills/benchmark-execution/SKILL.md +++ b/agents/nemo-studio-assistant/skills/benchmark-execution/SKILL.md @@ -4,7 +4,7 @@ description: "Benchmark task execution contract: complete every numbered require --- # Benchmark execution contract -This skill defines the execution requirements that every nemo-studio-copilot run under +This skill defines the execution requirements that every nemo-studio-assistant run under `tests/agentic-use/` must satisfy so the canonical gate (`tests/agentic-use/passrate_token_policy_gate.py`) can score the run on verifier pass-rate and token totals. See diff --git a/agents/nemo-studio-copilot/skills/entities/SKILL.md b/agents/nemo-studio-assistant/skills/entities/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot/skills/entities/SKILL.md rename to agents/nemo-studio-assistant/skills/entities/SKILL.md diff --git a/agents/nemo-studio-copilot/skills/evaluator/SKILL.md b/agents/nemo-studio-assistant/skills/evaluator/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot/skills/evaluator/SKILL.md rename to agents/nemo-studio-assistant/skills/evaluator/SKILL.md diff --git a/agents/nemo-studio-copilot/skills/files/SKILL.md b/agents/nemo-studio-assistant/skills/files/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot/skills/files/SKILL.md rename to agents/nemo-studio-assistant/skills/files/SKILL.md diff --git a/agents/nemo-studio-copilot/skills/guardrails/SKILL.md b/agents/nemo-studio-assistant/skills/guardrails/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot/skills/guardrails/SKILL.md rename to agents/nemo-studio-assistant/skills/guardrails/SKILL.md diff --git a/agents/nemo-studio-copilot/skills/inference/SKILL.md b/agents/nemo-studio-assistant/skills/inference/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot/skills/inference/SKILL.md rename to agents/nemo-studio-assistant/skills/inference/SKILL.md diff --git a/agents/nemo-studio-copilot/skills/secrets/SKILL.md b/agents/nemo-studio-assistant/skills/secrets/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot/skills/secrets/SKILL.md rename to agents/nemo-studio-assistant/skills/secrets/SKILL.md diff --git a/agents/nemo-studio-copilot/skills/workspace/SKILL.md b/agents/nemo-studio-assistant/skills/workspace/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot/skills/workspace/SKILL.md rename to agents/nemo-studio-assistant/skills/workspace/SKILL.md diff --git a/agents/nemo-studio-copilot/src/nemo_studio_copilot/mcp_server.py b/agents/nemo-studio-assistant/src/nemo_studio_assistant/mcp_server.py similarity index 82% rename from agents/nemo-studio-copilot/src/nemo_studio_copilot/mcp_server.py rename to agents/nemo-studio-assistant/src/nemo_studio_assistant/mcp_server.py index c5a81df6ac..9668d89462 100644 --- a/agents/nemo-studio-copilot/src/nemo_studio_copilot/mcp_server.py +++ b/agents/nemo-studio-assistant/src/nemo_studio_assistant/mcp_server.py @@ -1,10 +1,10 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Fabric-native MCP boundary for NeMo Studio Copilot tools.""" +"""Fabric-native MCP boundary for NeMo Studio Assistant tools.""" from fastmcp import FastMCP -from nemo_studio_copilot.register import ( +from nemo_studio_assistant.register import ( ask_user_question, check_status, job_progress, @@ -30,7 +30,7 @@ def create_server() -> FastMCP: - server = FastMCP("NeMo Studio Copilot") + server = FastMCP("NeMo Studio Assistant") for tool in TOOLS: server.tool(tool) return server diff --git a/agents/nemo-studio-copilot/src/nemo_studio_copilot/nemo-studio-copilot-eval-data.json b/agents/nemo-studio-assistant/src/nemo_studio_assistant/nemo-studio-assistant-eval-data.json similarity index 100% rename from agents/nemo-studio-copilot/src/nemo_studio_copilot/nemo-studio-copilot-eval-data.json rename to agents/nemo-studio-assistant/src/nemo_studio_assistant/nemo-studio-assistant-eval-data.json diff --git a/agents/nemo-studio-copilot/src/nemo_studio_copilot/register.py b/agents/nemo-studio-assistant/src/nemo_studio_assistant/register.py similarity index 99% rename from agents/nemo-studio-copilot/src/nemo_studio_copilot/register.py rename to agents/nemo-studio-assistant/src/nemo_studio_assistant/register.py index b9c4ce2aaf..a4d3431755 100644 --- a/agents/nemo-studio-copilot/src/nemo_studio_copilot/register.py +++ b/agents/nemo-studio-assistant/src/nemo_studio_assistant/register.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""NeMo Studio Copilot tool implementations exposed through MCP.""" +"""NeMo Studio Assistant tool implementations exposed through MCP.""" import json import os @@ -14,7 +14,7 @@ from pydantic import BaseModel DEFAULT_WORKSPACE = "default" -STUDIO_CALLBACK_PATH = "/studio/api/copilot/mcp/{session_id}" +STUDIO_CALLBACK_PATH = "/studio/api/assistant/mcp/{session_id}" STUDIO_CALLBACK_TIMEOUT_SECONDS = 3600.0 _READ_ONLY_SDK_ACTIONS = frozenset({"get", "get_logs", "get_status", "list", "read", "retrieve", "search"}) diff --git a/agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/.gitkeep b/agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/.gitkeep similarity index 100% rename from agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/.gitkeep rename to agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/.gitkeep diff --git a/agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/auditor/SKILL.md b/agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/auditor/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/auditor/SKILL.md rename to agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/auditor/SKILL.md diff --git a/agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/benchmark-execution/SKILL.md b/agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/benchmark-execution/SKILL.md similarity index 98% rename from agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/benchmark-execution/SKILL.md rename to agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/benchmark-execution/SKILL.md index 8c097a83f9..e5d8e6d17a 100644 --- a/agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/benchmark-execution/SKILL.md +++ b/agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/benchmark-execution/SKILL.md @@ -4,7 +4,7 @@ description: "Benchmark task execution contract: complete every numbered require --- # Benchmark execution contract -This skill defines the execution requirements that every nemo-studio-copilot run under +This skill defines the execution requirements that every nemo-studio-assistant run under `tests/agentic-use/` must satisfy so the canonical gate (`tests/agentic-use/passrate_token_policy_gate.py`) can score the run on verifier pass-rate and token totals. See diff --git a/agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/entities/SKILL.md b/agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/entities/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/entities/SKILL.md rename to agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/entities/SKILL.md diff --git a/agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/evaluator/SKILL.md b/agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/evaluator/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/evaluator/SKILL.md rename to agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/evaluator/SKILL.md diff --git a/agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/files/SKILL.md b/agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/files/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/files/SKILL.md rename to agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/files/SKILL.md diff --git a/agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/guardrails/SKILL.md b/agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/guardrails/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/guardrails/SKILL.md rename to agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/guardrails/SKILL.md diff --git a/agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/inference/SKILL.md b/agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/inference/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/inference/SKILL.md rename to agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/inference/SKILL.md diff --git a/agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/secrets/SKILL.md b/agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/secrets/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/secrets/SKILL.md rename to agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/secrets/SKILL.md diff --git a/agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/workspace/SKILL.md b/agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/workspace/SKILL.md similarity index 100% rename from agents/nemo-studio-copilot/src/nemo_studio_copilot/skills/workspace/SKILL.md rename to agents/nemo-studio-assistant/src/nemo_studio_assistant/skills/workspace/SKILL.md diff --git a/agents/nemo-studio-assistant/tests/smoke_test.md b/agents/nemo-studio-assistant/tests/smoke_test.md new file mode 100644 index 0000000000..1c01401fe5 --- /dev/null +++ b/agents/nemo-studio-assistant/tests/smoke_test.md @@ -0,0 +1,82 @@ +# NeMo Studio Assistant Fabric smoke test + +The assistant is a `nemo-agents-spec-v1` Fabric agent using the preinstalled +Deep Agents adapter. Its platform and Studio tools are exposed by the packaged +`nemo-studio-assistant-mcp` stdio server. + +## Static and unit validation + +From the repository root: + +```bash +PYTHONPATH=agents/nemo-studio-assistant/src \ + uv run --frozen pytest agents/nemo-studio-assistant/tests/test_nemo_studio_assistant.py -v +uv run ruff check agents/nemo-studio-assistant/src agents/nemo-studio-assistant/tests +uv run ruff format --check agents/nemo-studio-assistant/src agents/nemo-studio-assistant/tests +uv run --frozen ty check +``` + +The tests validate Fabric translation, the Deep Agents adapter, all packaged +skills, the MCP tool surface, SDK serialization, session validation, and +approval gating for mutations. + +## Register, package, and deploy + +These commands change platform state and should be run independently so each +result can be verified: + +```bash +export NMP_BASE_URL=http://127.0.0.1:8080 +nemo agents create --name nemo-studio-assistant \ + --agent-config agents/nemo-studio-assistant-spec/agent.yaml +nemo agents package \ + --agent agents/nemo-studio-assistant/agent.yaml \ + --pyproject agents/nemo-studio-assistant/pyproject.toml \ + --tag nemo-studio-assistant:fabric-local +nemo agents deploy --agent nemo-studio-assistant \ + --name nemo-studio-assistant-fabric-deployment \ + --mode docker --image nemo-studio-assistant:fabric-local +nemo agents deployments wait nemo-studio-assistant-fabric-deployment +``` + +The generated image must use the Fabric server entrypoint, not `nat serve`. + +## Invocation + +```bash +nemo agents invoke --agent-deployment nemo-studio-assistant-fabric-deployment \ + --input "List all workspaces on the platform" +nemo agents invoke --agent-deployment nemo-studio-assistant-fabric-deployment \ + --input "List the available models and inference providers using the platform API." +``` + +For mutation approval, invoke through Studio and ask the assistant to create a +workspace named `nemo-studio-assistant-smoke-test`. Verify that Studio renders an +approval prompt before the SDK call, and that declining leaves platform state +unchanged. + +## Studio streaming + +Open `http://127.0.0.1:8080/studio`, select the assistant deployment, and send +`List all workspaces`. Verify incremental, nonduplicated text and a terminal +success or error. Then exercise a selector and an approved mutation to verify +the callback MCP tools. + +## Cleanup + +After verification, remove the temporary workspace, then undeploy and delete +the agent so another smoke-test run starts cleanly: + +```bash +nemo workspaces delete nemo-studio-assistant-smoke-test +nemo agents undeploy nemo-studio-assistant-fabric-deployment --yes +nemo agents delete nemo-studio-assistant --yes +``` + +## Automated evaluation coverage + +The former NAT `/generate/full` evaluation YAML remains removed. Each case in +`src/nemo_studio_assistant/nemo-studio-assistant-eval-data.json` now has +deterministic unit coverage for its required `nemo_api` tool path in +`test_nemo_studio_assistant.py`. Root pytest discovery includes these unit tests, +so `make test-unit-ci` runs them in CI without a live model endpoint. diff --git a/agents/nemo-studio-copilot/tests/test_nemo_studio_copilot.py b/agents/nemo-studio-assistant/tests/test_nemo_studio_assistant.py similarity index 97% rename from agents/nemo-studio-copilot/tests/test_nemo_studio_copilot.py rename to agents/nemo-studio-assistant/tests/test_nemo_studio_assistant.py index c4c8de1830..dfcc66523d 100644 --- a/agents/nemo-studio-copilot/tests/test_nemo_studio_copilot.py +++ b/agents/nemo-studio-assistant/tests/test_nemo_studio_assistant.py @@ -9,12 +9,12 @@ import pytest from nemo_agents_plugin.agent_config import load_agent_config from nemo_agents_plugin.fabric.translator import translate_agent_config -from nemo_studio_copilot import register -from nemo_studio_copilot.mcp_server import create_server +from nemo_studio_assistant import register +from nemo_studio_assistant.mcp_server import create_server AGENT_ROOT = Path(__file__).parents[1] -SPEC_ROOT = AGENT_ROOT.parent / "nemo-studio-copilot-spec" -EVAL_DATA_PATH = AGENT_ROOT / "src/nemo_studio_copilot/nemo-studio-copilot-eval-data.json" +SPEC_ROOT = AGENT_ROOT.parent / "nemo-studio-assistant-spec" +EVAL_DATA_PATH = AGENT_ROOT / "src/nemo_studio_assistant/nemo-studio-assistant-eval-data.json" pytestmark = pytest.mark.unit @@ -65,7 +65,7 @@ def test_agent_config_translates_to_fabric_deepagents() -> None: assert translated.models["default"].provider == "nvidia" assert translated.mcp is not None assert translated.mcp.servers["nemo_studio"].url == ( - "env NMP_BASE_URL=$NMP_BASE_URL NMP_WORKSPACE=$NMP_WORKSPACE nemo-studio-copilot-mcp" + "env NMP_BASE_URL=$NMP_BASE_URL NMP_WORKSPACE=$NMP_WORKSPACE nemo-studio-assistant-mcp" ) @@ -398,7 +398,7 @@ def test_studio_callback_url_uses_workspace(monkeypatch: pytest.MonkeyPatch) -> url = register._studio_callback_url(session_id, workspace="demo") - assert url == f"http://platform:8080/studio/api/copilot/mcp/{session_id}?workspace=demo" + assert url == f"http://platform:8080/studio/api/assistant/mcp/{session_id}?workspace=demo" def test_ask_user_question_rejects_invalid_payload() -> None: diff --git a/agents/nemo-studio-copilot/uv.lock b/agents/nemo-studio-assistant/uv.lock similarity index 99% rename from agents/nemo-studio-copilot/uv.lock rename to agents/nemo-studio-assistant/uv.lock index bc0063e2bc..39b2acfc50 100644 --- a/agents/nemo-studio-copilot/uv.lock +++ b/agents/nemo-studio-assistant/uv.lock @@ -3457,7 +3457,7 @@ wheels = [ ] [[package]] -name = "nemo-studio-copilot" +name = "nemo-studio-assistant" version = "0.1.0" source = { editable = "." } dependencies = [ diff --git a/agents/nemo-studio-copilot/tests/smoke_test.md b/agents/nemo-studio-copilot/tests/smoke_test.md deleted file mode 100644 index 63780b96f2..0000000000 --- a/agents/nemo-studio-copilot/tests/smoke_test.md +++ /dev/null @@ -1,82 +0,0 @@ -# NeMo Studio Copilot Fabric smoke test - -The copilot is a `nemo-agents-spec-v1` Fabric agent using the preinstalled -Deep Agents adapter. Its platform and Studio tools are exposed by the packaged -`nemo-studio-copilot-mcp` stdio server. - -## Static and unit validation - -From the repository root: - -```bash -PYTHONPATH=agents/nemo-studio-copilot/src \ - uv run --frozen pytest agents/nemo-studio-copilot/tests/test_nemo_studio_copilot.py -v -uv run ruff check agents/nemo-studio-copilot/src agents/nemo-studio-copilot/tests -uv run ruff format --check agents/nemo-studio-copilot/src agents/nemo-studio-copilot/tests -uv run --frozen ty check -``` - -The tests validate Fabric translation, the Deep Agents adapter, all packaged -skills, the MCP tool surface, SDK serialization, session validation, and -approval gating for mutations. - -## Register, package, and deploy - -These commands change platform state and should be run independently so each -result can be verified: - -```bash -export NMP_BASE_URL=http://127.0.0.1:8080 -nemo agents create --name nemo-studio-copilot \ - --agent-config agents/nemo-studio-copilot-spec/agent.yaml -nemo agents package \ - --agent agents/nemo-studio-copilot/agent.yaml \ - --pyproject agents/nemo-studio-copilot/pyproject.toml \ - --tag nemo-studio-copilot:fabric-local -nemo agents deploy --agent nemo-studio-copilot \ - --name nemo-studio-copilot-fabric-deployment \ - --mode docker --image nemo-studio-copilot:fabric-local -nemo agents deployments wait nemo-studio-copilot-fabric-deployment -``` - -The generated image must use the Fabric server entrypoint, not `nat serve`. - -## Invocation - -```bash -nemo agents invoke --agent-deployment nemo-studio-copilot-fabric-deployment \ - --input "List all workspaces on the platform" -nemo agents invoke --agent-deployment nemo-studio-copilot-fabric-deployment \ - --input "List the available models and inference providers using the platform API." -``` - -For mutation approval, invoke through Studio and ask the copilot to create a -workspace named `nemo-studio-copilot-smoke-test`. Verify that Studio renders an -approval prompt before the SDK call, and that declining leaves platform state -unchanged. - -## Studio streaming - -Open `http://127.0.0.1:8080/studio`, select the copilot deployment, and send -`List all workspaces`. Verify incremental, nonduplicated text and a terminal -success or error. Then exercise a selector and an approved mutation to verify -the callback MCP tools. - -## Cleanup - -After verification, remove the temporary workspace, then undeploy and delete -the agent so another smoke-test run starts cleanly: - -```bash -nemo workspaces delete nemo-studio-copilot-smoke-test -nemo agents undeploy nemo-studio-copilot-fabric-deployment --yes -nemo agents delete nemo-studio-copilot --yes -``` - -## Automated evaluation coverage - -The former NAT `/generate/full` evaluation YAML remains removed. Each case in -`src/nemo_studio_copilot/nemo-studio-copilot-eval-data.json` now has -deterministic unit coverage for its required `nemo_api` tool path in -`test_nemo_studio_copilot.py`. Root pytest discovery includes these unit tests, -so `make test-unit-ci` runs them in CI without a live model endpoint. diff --git a/docs/about/release-notes/current-release.mdx b/docs/about/release-notes/current-release.mdx index 010fc5de55..b43fd496e8 100644 --- a/docs/about/release-notes/current-release.mdx +++ b/docs/about/release-notes/current-release.mdx @@ -113,7 +113,7 @@ available as a legacy path. - Safe Synthesizer reports include score-driven gauges. - Studio can load plugin web bundles through `/apis/plugins` and render them inside the Studio React tree with trusted bundle URL checks. -- NeMo Studio Copilot adds a packaged agent, chat history, tool-call rendering, +- NeMo Studio Assistant adds a packaged agent, chat history, tool-call rendering, and reasoning display for Studio-assisted workflows. ### Platform, CLI, and Deployment diff --git a/k8s/helm/README.md b/k8s/helm/README.md index 5653902791..9942b056ed 100644 --- a/k8s/helm/README.md +++ b/k8s/helm/README.md @@ -401,7 +401,7 @@ and | openshiftRoute.service | string | `"{{ include \"nemo-platform.ingressBackendService\" . }}"` | Service name to route to. Defaults to Envoy when auth+envoy enabled, otherwise API (tpl-evaluated). | | openshiftRoute.targetPort | string | `"{{ include \"nemo-platform.ingressBackendPort\" . }}"` | Target port on the service. Defaults to Envoy or API port depending on auth (tpl-evaluated). | | openshiftRoute.tls | object | `{}` | Optional TLS configuration (termination, certificate, key, etc.). See OpenShift Route spec. | -| platformConfig | object | `{}` | Platform-wide configuration settings Set configuration here to apply custom, structured configuration across all services. Applied after the base platform config is evaluated for templates. Enables adding / overriding YAML-based elements in the evaluated platform config. It is usually recommended to use this config section instead of `basePlatformConfig` unless you need to use templating features. For example, you can set the NIM default StorageClass via models.controller.backends.deployments_plugin.default_storage_class. For full configuration reference, see https://docs.nvidia.com/nemo-platform | +| platformConfig | object | `{"studio":{"feature_flags":{"assistant_studio_enabled":true}}}` | Platform-wide configuration settings Set configuration here to apply custom, structured configuration across all services. Applied after the base platform config is evaluated for templates. Enables adding / overriding YAML-based elements in the evaluated platform config. It is usually recommended to use this config section instead of `basePlatformConfig` unless you need to use templating features. For example, you can set the NIM default StorageClass via models.controller.backends.deployments_plugin.default_storage_class. For full configuration reference, see https://docs.nvidia.com/nemo-platform | | platformSeedJob | object | This object has the following default values for the platform seed Job configuration. | Platform seed Job (Helm hook: runs after install/upgrade) Runs the platform-seed task (guardrails configs, evaluator system entities, data designer filesets). Uses post-install,post-upgrade hooks so it runs on fresh installs and can be re-triggered on no-op upgrade. | | platformSeedJob.activeDeadlineSeconds | int | `600` | Maximum time in seconds the Job can run. | | platformSeedJob.affinity | object | `{}` | Affinity for the platform seeding Job pod. | diff --git a/k8s/helm/values.yaml b/k8s/helm/values.yaml index adcdc892ee..424909e49c 100644 --- a/k8s/helm/values.yaml +++ b/k8s/helm/values.yaml @@ -366,7 +366,10 @@ externalClickhouse: # It is usually recommended to use this config section instead of `basePlatformConfig` unless you need to use templating features. # For example, you can set the NIM default StorageClass via models.controller.backends.deployments_plugin.default_storage_class. # For full configuration reference, see https://docs.nvidia.com/nemo-platform -platformConfig: {} +platformConfig: + studio: + feature_flags: + assistant_studio_enabled: true # -- Base platform configuration settings # @default -- This object has the following default values for the base platform configuration. diff --git a/packages/nmp_platform/config/local.yaml b/packages/nmp_platform/config/local.yaml index 8caa44d0fe..de6bb5655f 100644 --- a/packages/nmp_platform/config/local.yaml +++ b/packages/nmp_platform/config/local.yaml @@ -186,4 +186,4 @@ studio: static_files_path: web/packages/studio/dist sandbox_enabled: true feature_flags: - copilot_studio_enabled: true + assistant_studio_enabled: true diff --git a/pyproject.toml b/pyproject.toml index 84452d8779..60be65466e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -587,9 +587,9 @@ extra-paths = [ # nemo-agents plugin is not a workspace member; add its src so ty can # resolve nemo_agents_plugin imports when checking plugin test files. "plugins/nemo-agents/src", - # The built-in NeMo Copilot is a standalone deployable with its own lockfile; - # add its src so ty can resolve nemo_studio_copilot imports from its tests. - "agents/nemo-studio-copilot/src", + # The built-in NeMo Assistant is a standalone deployable with its own lockfile; + # add its src so ty can resolve nemo_studio_assistant imports from its tests. + "agents/nemo-studio-assistant/src", # example-plugin is not a workspace member; add its src so ty can # resolve nmp.example_plugin imports when checking plugin test files. "plugins/example-plugin/src", diff --git a/pytest.ini b/pytest.ini index aa7ea14bec..e58d0906ce 100644 --- a/pytest.ini +++ b/pytest.ini @@ -8,7 +8,7 @@ python_classes = Test* python_functions = test_* pythonpath = . - agents/nemo-studio-copilot/src + agents/nemo-studio-assistant/src plugins/example-plugin/src plugins/nemo-deployments/src plugins/nemo-deployments/tests/unit @@ -27,7 +27,7 @@ pythonpath = # - packages/nemo_nb/tests (3 collection errors) # - Some core/infrastructure tests (module dependencies) testpaths = - agents/nemo-studio-copilot/tests + agents/nemo-studio-assistant/tests packages/nemo_platform_plugin/tests packages/nmp_platform/tests packages/nmp_platform_runner/tests diff --git a/services/studio/src/nmp/studio/copilot.py b/services/studio/src/nmp/studio/assistant.py similarity index 92% rename from services/studio/src/nmp/studio/copilot.py rename to services/studio/src/nmp/studio/assistant.py index 1bc13c32fa..f5e35b2c60 100644 --- a/services/studio/src/nmp/studio/copilot.py +++ b/services/studio/src/nmp/studio/assistant.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Local copilot bridge for Studio.""" +"""Local assistant bridge for Studio.""" import ast import asyncio @@ -26,12 +26,12 @@ from nmp.common.entities.constants import NAME_PATTERN from nmp.common.service.dependencies import get_entity_client from nmp.studio import studio_links -from nmp.studio.copilot_artifacts import ( +from nmp.studio.assistant_artifacts import ( ChatArtifactsResponse, InputSelectionTool, answer_selection_pairs, record_answer_selections, - record_copilot_model, + record_assistant_model, record_spec_text_artifacts, record_tool_artifacts, record_tool_name, @@ -39,7 +39,7 @@ record_workspace_artifact, string_value, ) -from nmp.studio.copilot_mcp_tools import ( +from nmp.studio.assistant_mcp_tools import ( APPROVAL_TOOL_NAME, CLAUDE_MCP_SERVER_NAME, JOB_PROGRESS_TOOL_NAME, @@ -48,29 +48,29 @@ SELECT_DATASET_FILE_TOOL_NAME, SELECT_EVAL_CONFIG_TOOL_NAME, SELECT_MODEL_TOOL_NAME, - STUDIO_COPILOT_CONTEXT, + STUDIO_ASSISTANT_CONTEXT, STUDIO_LINK_TOOL_NAME, allowed_mcp_tools, permission_prompt_tool, ) -from nmp.studio.copilot_skills import ClaudeSkillResponse, DuplicateSkillError, list_claude_skill_responses -from nmp.studio.entities import CopilotConversation, CopilotMessage +from nmp.studio.assistant_skills import ClaudeSkillResponse, DuplicateSkillError, list_claude_skill_responses +from nmp.studio.entities import AssistantConversation, AssistantMessage, LegacyAssistantConversation from pydantic import BaseModel, ConfigDict, Field from starlette.routing import NoMatchFound logger = logging.getLogger(__name__) -router = APIRouter(prefix="/v2/copilot") +router = APIRouter(prefix="/v2/assistant") -MCP_ROUTE_NAME = "studio_copilot_mcp" -PUBLIC_MCP_ROUTE_NAME = "studio_copilot_public_mcp" -PUBLIC_MCP_UNSUPPORTED_METHOD_ROUTE_NAME = "studio_copilot_public_mcp_unsupported_method" -PUBLIC_MCP_PATH = "/studio/api/copilot/mcp/{session_id}" +MCP_ROUTE_NAME = "studio_assistant_mcp" +PUBLIC_MCP_ROUTE_NAME = "studio_assistant_public_mcp" +PUBLIC_MCP_UNSUPPORTED_METHOD_ROUTE_NAME = "studio_assistant_public_mcp_unsupported_method" +PUBLIC_MCP_PATH = "/studio/api/assistant/mcp/{session_id}" CLAUDE_MCP_TOOL_TIMEOUT_MS = 2_147_483_647 MCP_KEEPALIVE_INTERVAL_SECONDS = 15 -DEFAULT_STUDIO_COPILOT_NAME = "nemo-studio-copilot" -DEFAULT_STUDIO_COPILOT_BASE_URL = "http://127.0.0.1:8080" -STUDIO_COPILOT_TIMEOUT_SECONDS = 600.0 +DEFAULT_STUDIO_ASSISTANT_NAME = "nemo-studio-assistant" +DEFAULT_STUDIO_ASSISTANT_BASE_URL = "http://127.0.0.1:8080" +STUDIO_ASSISTANT_TIMEOUT_SECONDS = 600.0 MAX_RETAINED_SESSIONS = 100 MAX_RETAINED_TURNS_PER_SESSION = 50 @@ -85,13 +85,13 @@ class NewSessionResponse(BaseModel): - """Response returned when Studio starts a new copilot session.""" + """Response returned when Studio starts a new assistant session.""" session_id: str class MessageRequest(BaseModel): - """A user message to send to the local copilot.""" + """A user message to send to the local assistant.""" message: str = Field(min_length=1) studio_base_url: str | None = Field(default=None, min_length=1) @@ -117,7 +117,7 @@ class AgentInputDecision(BaseModel): class HistorySessionResponse(BaseModel): - """Summary of a persisted Copilot or legacy Claude session.""" + """Summary of a persisted Assistant or legacy Claude session.""" session_id: str mtime: float @@ -131,7 +131,7 @@ class HistorySessionResponse(BaseModel): class SessionHistoryResponse(BaseModel): - """Copilot session history normalized for Studio chat replay.""" + """Assistant session history normalized for Studio chat replay.""" session_id: str items: list[dict[str, Any]] @@ -151,25 +151,25 @@ class SessionHistoryResponse(BaseModel): _AGENT_INPUT_RESPONSE_RESERVED_KEYS = frozenset({"message", "status"}) -def _recent_conversation_messages(conversation: list[CopilotMessage]) -> list[CopilotMessage]: +def _recent_conversation_messages(conversation: list[AssistantMessage]) -> list[AssistantMessage]: """Bound model context without truncating the persisted chat history.""" max_messages = MAX_RETAINED_TURNS_PER_SESSION * 2 return conversation[-max_messages:] def _append_conversation_turn( - conversation: CopilotConversation, + conversation: AssistantConversation, user_message: str, assistant_message: str, model: str, ) -> None: conversation.messages.extend( [ - CopilotMessage(role="user", content=user_message), - CopilotMessage(role="assistant", content=assistant_message), + AssistantMessage(role="user", content=user_message), + AssistantMessage(role="assistant", content=assistant_message), ] ) - record_copilot_model(conversation.chat_artifacts, model) + record_assistant_model(conversation.chat_artifacts, model) @dataclass @@ -226,9 +226,35 @@ def _validate_session_id(session_id: str) -> str: def _conversation_name(session_id: str) -> str: """Return the Entity Store name for a Studio session UUID.""" + return f"assistant-{session_id}" + + +def _legacy_conversation_name(session_id: str) -> str: + """Return the Entity Store name used before the Assistant rename.""" return f"copilot-{session_id}" +async def _get_conversation( + entity_store: EntityClient, + *, + session_id: str, + workspace: str, +) -> AssistantConversation: + """Load a current conversation, falling back to its pre-rename identity.""" + try: + return await entity_store.get( + AssistantConversation, + _conversation_name(session_id), + workspace=workspace, + ) + except EntityNotFoundError: + return await entity_store.get( + LegacyAssistantConversation, + _legacy_conversation_name(session_id), + workspace=workspace, + ) + + def _request_principal_id(request: Request) -> str: """Return the end-user principal, including service-on-behalf-of requests.""" return ( @@ -242,12 +268,12 @@ async def _get_owned_conversation( session_id: str, workspace: str, owner_id: str, -) -> CopilotConversation: +) -> AssistantConversation: """Load a conversation and enforce per-user ownership within a workspace.""" try: - conversation = await entity_store.get( - CopilotConversation, - _conversation_name(session_id), + conversation = await _get_conversation( + entity_store, + session_id=session_id, workspace=workspace, ) except EntityNotFoundError as exc: @@ -369,7 +395,7 @@ def _build_studio_system_prompt( current_studio_route = _trimmed_string(studio_pathname) or "unknown" destinations = studio_links.STUDIO_LINK_DESTINATIONS if enabled_destinations is None else enabled_destinations lines = [ - "You are NeMo Copilot, running inside NeMo Studio.", + "You are NeMo Assistant, running inside NeMo Studio.", f"Current Studio workspace: {workspace or 'unknown'}", f"Studio UI base URL: {normalized_base_url or 'unknown'}", f"Current Studio route path: {current_studio_route}", @@ -471,15 +497,15 @@ def _build_studio_system_prompt( return "\n".join(lines) -def _build_copilot_system_prompt( +def _build_assistant_system_prompt( session_id: str, workspace: str | None, studio_base_url: str | None, studio_pathname: str | None, enabled_destinations: Mapping[str, studio_links.StudioLinkDestination] | None = None, ) -> str: - """Adapt the existing Studio copilot rules to the deployed NeMo Copilot tools.""" - context = STUDIO_COPILOT_CONTEXT + """Adapt the existing Studio assistant rules to the deployed NeMo Assistant tools.""" + context = STUDIO_ASSISTANT_CONTEXT prompt = _build_studio_system_prompt( workspace, studio_base_url, @@ -507,9 +533,9 @@ def _build_copilot_system_prompt( [ context, prompt, - "Your identity in this interface is NeMo Copilot.", + "Your identity in this interface is NeMo Assistant.", "Do not describe yourself as a different assistant or provider.", - "Deployed NeMo Copilot callback behavior:", + "Deployed NeMo Assistant callback behavior:", ( f"For every select_agent, select_model, select_dataset_file, select_eval_config, " f"job_progress, studio_link, and ask_user_question call, pass studio_session_id='{session_id}'." @@ -651,7 +677,7 @@ def _summarize_history_session(path: Path) -> HistorySummary: message = entry.get("message") if isinstance(message, dict): - record_copilot_model(summary.chat_artifacts, string_value(message.get("model"))) + record_assistant_model(summary.chat_artifacts, string_value(message.get("model"))) usage_identity = _usage_identity(entry, message) if usage_identity is None or usage_identity not in seen_usage_events: summary.token_count += _usage_token_count(message.get("usage")) @@ -726,11 +752,11 @@ async def create_session( workspace: str = "default", entity_store: EntityClient = Depends(get_entity_client), ) -> NewSessionResponse: - """Create a durable, user-owned Copilot session.""" + """Create a durable, user-owned Assistant session.""" workspace = _validated_workspace_or_default(workspace) session_id = str(uuid.uuid4()) await entity_store.create( - CopilotConversation( + AssistantConversation( name=_conversation_name(session_id), workspace=workspace, session_id=session_id, @@ -746,18 +772,25 @@ async def list_history_sessions( workspace: str = "default", entity_store: EntityClient = Depends(get_entity_client), ) -> list[HistorySessionResponse]: - """List the current user's durable NeMo Copilot sessions.""" + """List the current user's durable NeMo Assistant sessions.""" workspace = _validated_workspace_or_default(workspace) owner_id = _request_principal_id(request) - result = await entity_store.list( - CopilotConversation, - workspace=workspace, - filter_obj={"owner_id": owner_id}, - sort="-updated_at", - page_size=MAX_RETAINED_SESSIONS, - ) + results = [ + await entity_store.list( + entity_type, + workspace=workspace, + filter_obj={"owner_id": owner_id}, + sort="-updated_at", + page_size=MAX_RETAINED_SESSIONS, + ) + for entity_type in (AssistantConversation, LegacyAssistantConversation) + ] sessions: list[HistorySessionResponse] = [] - for conversation in result.data: + seen_session_ids: set[str] = set() + for conversation in (conversation for result in results for conversation in result.data): + if conversation.session_id in seen_session_ids: + continue + seen_session_ids.add(conversation.session_id) user_messages = [message.content for message in conversation.messages if message.role == "user"] if not user_messages: continue @@ -861,15 +894,11 @@ async def get_session_history( workspace: str = "default", entity_store: EntityClient = Depends(get_entity_client), ) -> SessionHistoryResponse: - """Load a NeMo Copilot session or legacy Claude history for replay.""" + """Load a NeMo Assistant session or legacy Claude history for replay.""" sid = _validate_session_id(session_id) workspace = _validated_workspace_or_default(workspace) try: - conversation = await entity_store.get( - CopilotConversation, - _conversation_name(sid), - workspace=workspace, - ) + conversation = await _get_conversation(entity_store, session_id=sid, workspace=workspace) except EntityNotFoundError: conversation = None if conversation is not None: @@ -951,7 +980,7 @@ async def delete_session_history( workspace: str = "default", entity_store: EntityClient = Depends(get_entity_client), ) -> Response: - """Delete the current user's persisted Copilot conversation.""" + """Delete the current user's persisted Assistant conversation.""" sid = _validate_session_id(session_id) workspace = _validated_workspace_or_default(workspace) if sid in _session_streams: @@ -964,7 +993,7 @@ async def delete_session_history( ) try: await entity_store.delete( - CopilotConversation, + type(conversation), conversation.name, workspace=workspace, expected_db_version=conversation.db_version, @@ -1003,7 +1032,7 @@ def _mcp_url( return f"{url}?{urlencode(query_params)}" if query_params else url except NoMatchFound: continue - raise RuntimeError("Studio copilot MCP route is not mounted") + raise RuntimeError("Studio assistant MCP route is not mounted") def _build_claude_argv( @@ -1039,7 +1068,7 @@ def _build_claude_argv( "--allowedTools", ",".join(allowed_mcp_tools(CLAUDE_MCP_SERVER_NAME)), "--append-system-prompt", - STUDIO_COPILOT_CONTEXT, + STUDIO_ASSISTANT_CONTEXT, "--permission-prompt-tool", permission_prompt_tool(CLAUDE_MCP_SERVER_NAME), ] @@ -1120,7 +1149,7 @@ async def _blocking_mcp_tool_response( async def _request_permission(session_id: str, args: dict[str, Any]) -> dict[str, Any]: queue = _session_streams.get(session_id) if queue is None: - return {"behavior": "deny", "message": "no active Studio copilot session"} + return {"behavior": "deny", "message": "no active Studio assistant session"} request_id = str(uuid.uuid4()) loop = asyncio.get_running_loop() @@ -1153,7 +1182,7 @@ async def _request_permission(session_id: str, args: dict[str, Any]) -> dict[str async def _request_agent_input(session_id: str, kind: str, args: dict[str, Any]) -> dict[str, Any]: queue = _session_streams.get(session_id) if queue is None: - return {"status": "error", "message": "no active Studio copilot session"} + return {"status": "error", "message": "no active Studio assistant session"} request_id = str(uuid.uuid4()) loop = asyncio.get_running_loop() @@ -1320,15 +1349,15 @@ async def _stream_claude( task.cancel() -def _studio_copilot_name() -> str: - return _trimmed_string(os.environ.get("STUDIO_COPILOT_NAME")) or DEFAULT_STUDIO_COPILOT_NAME +def _studio_assistant_name() -> str: + return _trimmed_string(os.environ.get("STUDIO_ASSISTANT_NAME")) or DEFAULT_STUDIO_ASSISTANT_NAME -def _studio_copilot_base_url() -> str: +def _studio_assistant_base_url() -> str: base_url = ( - _trimmed_string(os.environ.get("STUDIO_COPILOT_BASE_URL")) + _trimmed_string(os.environ.get("STUDIO_ASSISTANT_BASE_URL")) or _trimmed_string(os.environ.get("NMP_BASE_URL")) - or DEFAULT_STUDIO_COPILOT_BASE_URL + or DEFAULT_STUDIO_ASSISTANT_BASE_URL ) parsed = urlparse(base_url) if ( @@ -1339,7 +1368,7 @@ def _studio_copilot_base_url() -> str: or parsed.query or parsed.fragment ): - raise RuntimeError("The configured copilot base URL is invalid") + raise RuntimeError("The configured assistant base URL is invalid") return base_url.rstrip("/") @@ -1391,7 +1420,7 @@ async def _authorized_workspace(workspace: str, headers: Mapping[str, str], sess if cached is not None: return cached - base_url = _studio_copilot_base_url() + base_url = _studio_assistant_base_url() list_url = f"{base_url}/apis/entities/v2/workspaces" timeout = httpx.Timeout(connect=10.0, read=30.0, write=30.0, pool=10.0) try: @@ -1430,16 +1459,16 @@ def _workspace_path_segment(workspace: str) -> str: return quote(workspace, safe="") -def _studio_copilot_url(workspace: str) -> str: +def _studio_assistant_url(workspace: str) -> str: # ``workspace`` is expected to already be an Entity-Store-confirmed name (see # _authorized_workspace); it is percent-encoded as a single path segment here. return ( - f"{_studio_copilot_base_url()}/apis/agents/v2/workspaces/{_workspace_path_segment(workspace)}" - f"/agents/{quote(_studio_copilot_name(), safe='')}/-/v1/chat/completions" + f"{_studio_assistant_base_url()}/apis/agents/v2/workspaces/{_workspace_path_segment(workspace)}" + f"/agents/{quote(_studio_assistant_name(), safe='')}/-/v1/chat/completions" ) -def _copilot_request_headers(request: Request, agent_url: str) -> dict[str, str]: +def _assistant_request_headers(request: Request, agent_url: str) -> dict[str, str]: """Forward end-user auth context only over an encrypted connection.""" if urlparse(agent_url).scheme != "https": return {} @@ -1452,29 +1481,29 @@ def _copilot_request_headers(request: Request, agent_url: str) -> dict[str, str] return forwarded -def _copilot_response(body: Any) -> tuple[str, str]: +def _assistant_response(body: Any) -> tuple[str, str]: if not isinstance(body, dict): - raise RuntimeError("NeMo Copilot returned a non-object response") + raise RuntimeError("NeMo Assistant returned a non-object response") choices = body.get("choices") if not isinstance(choices, list) or not choices or not isinstance(choices[0], dict): - raise RuntimeError("NeMo Copilot response did not include a choice") + raise RuntimeError("NeMo Assistant response did not include a choice") message = choices[0].get("message") if not isinstance(message, dict) or not isinstance(message.get("content"), str): - raise RuntimeError("NeMo Copilot response did not include assistant text") - model = _trimmed_string(body.get("model")) or _studio_copilot_name() + raise RuntimeError("NeMo Assistant response did not include assistant text") + model = _trimmed_string(body.get("model")) or _studio_assistant_name() return message["content"], model -def _copilot_error_detail(exc: httpx.HTTPError | RuntimeError | ValueError) -> str: +def _assistant_error_detail(exc: httpx.HTTPError | RuntimeError | ValueError) -> str: """Return a safe client-facing error without leaking exception or upstream response details.""" if isinstance(exc, httpx.HTTPStatusError): - return f"The deployed NeMo Copilot returned HTTP {exc.response.status_code}." + return f"The deployed NeMo Assistant returned HTTP {exc.response.status_code}." if isinstance(exc, httpx.HTTPError): - return "The deployed NeMo Copilot could not be reached." - return "The deployed NeMo Copilot returned an invalid response." + return "The deployed NeMo Assistant could not be reached." + return "The deployed NeMo Assistant returned an invalid response." -def _copilot_request_payload( +def _assistant_request_payload( messages: list[dict[str, str]], studio_session_id: str, ) -> dict[str, Any]: @@ -1547,8 +1576,8 @@ def _reasoning_stream_event(reasoning: str) -> tuple[str, str]: { "type": "assistant", "message": { - "id": f"nemo-copilot-reasoning-{uuid.uuid4()}", - "model": _studio_copilot_name(), + "id": f"nemo-assistant-reasoning-{uuid.uuid4()}", + "model": _studio_assistant_name(), "content": [{"type": "reasoning", "text": reasoning}], }, } @@ -1596,8 +1625,8 @@ def _tool_use_stream_event(tool_name: str, tool_input: dict[str, Any]) -> tuple[ { "type": "assistant", "message": { - "id": f"nemo-copilot-tool-{uuid.uuid4()}", - "model": _studio_copilot_name(), + "id": f"nemo-assistant-tool-{uuid.uuid4()}", + "model": _studio_assistant_name(), "content": [ { "type": "tool_use", @@ -1612,7 +1641,7 @@ def _tool_use_stream_event(tool_name: str, tool_input: dict[str, Any]) -> tuple[ ) -async def _invoke_copilot( +async def _invoke_assistant( agent_url: str, headers: Mapping[str, str], messages: list[dict[str, str]], @@ -1620,13 +1649,13 @@ async def _invoke_copilot( ) -> tuple[str, str]: timeout = httpx.Timeout( connect=10.0, - read=STUDIO_COPILOT_TIMEOUT_SECONDS, + read=STUDIO_ASSISTANT_TIMEOUT_SECONDS, write=60.0, pool=10.0, ) queue = _session_streams.get(studio_session_id) content_parts: list[str] = [] - model = _studio_copilot_name() + model = _studio_assistant_name() seen_tool_ids: set[str] = set() seen_reasoning_ids: set[str] = set() async with httpx.AsyncClient(timeout=timeout) as client: @@ -1636,7 +1665,7 @@ async def _invoke_copilot( "POST", agent_url, headers=dict(headers), - json=_copilot_request_payload(messages, studio_session_id), + json=_assistant_request_payload(messages, studio_session_id), ) as response: response.raise_for_status() async for line in response.aiter_lines(): @@ -1712,16 +1741,16 @@ def _render_session_event(event_type: str, payload: Any) -> str | None: return None -async def _stream_copilot( +async def _stream_assistant( session_id: str, message: str, agent_url: str, headers: Mapping[str, str], studio_system_prompt: str, - conversation: CopilotConversation, + conversation: AssistantConversation, entity_store: EntityClient, ) -> AsyncIterator[str]: - """Invoke the deployed NeMo Copilot while preserving Studio's blocking UI event protocol.""" + """Invoke the deployed NeMo Assistant while preserving Studio's blocking UI event protocol.""" if session_id in _session_streams: yield _sse( json.dumps({"message": "session already has an active stream"}), @@ -1745,7 +1774,7 @@ async def _stream_copilot( {"role": "user", "content": contextual_message}, ] invocation = asyncio.create_task( - _invoke_copilot( + _invoke_assistant( agent_url, headers, request_messages, @@ -1793,9 +1822,9 @@ async def _stream_copilot( try: await entity_store.update(conversation) except EntityConflictError: - logger.info("Reloading conflicted NeMo Copilot session %s before retrying", session_id) + logger.info("Reloading conflicted NeMo Assistant session %s before retrying", session_id) latest_conversation = await entity_store.get( - CopilotConversation, + AssistantConversation, conversation.name, workspace=conversation.workspace, ) @@ -1806,7 +1835,7 @@ async def _stream_copilot( { "type": "assistant", "message": { - "id": f"nemo-studio-copilot-{uuid.uuid4()}", + "id": f"nemo-studio-assistant-{uuid.uuid4()}", "model": model, "content": [{"type": "text", "text": assistant_text}], }, @@ -1818,15 +1847,15 @@ async def _stream_copilot( invocation.cancel() raise except (httpx.HTTPError, RuntimeError, ValueError) as exc: - logger.exception("NeMo Copilot invocation failed for session %s", session_id) + logger.exception("NeMo Assistant invocation failed for session %s", session_id) yield _sse( - json.dumps({"message": _copilot_error_detail(exc)}), + json.dumps({"message": _assistant_error_detail(exc)}), event="error", ) except EntityStoreError: - logger.exception("Failed to persist NeMo Copilot session %s", session_id) + logger.exception("Failed to persist NeMo Assistant session %s", session_id) yield _sse( - json.dumps({"message": "NeMo Copilot could not save this conversation."}), + json.dumps({"message": "NeMo Assistant could not save this conversation."}), event="error", ) finally: @@ -1844,13 +1873,13 @@ async def send_message( request: Request, entity_store: EntityClient = Depends(get_entity_client), ) -> StreamingResponse: - """Send a message to the deployed NeMo Copilot and stream Studio events.""" + """Send a message to the deployed NeMo Assistant and stream Studio events.""" sid = _validate_session_id(session_id) workspace = _validated_workspace_or_default(body.workspace) # Headers are forwarded only over HTTPS, and the scheme comes from the # server-configured base URL, so deriving them before the workspace is # confirmed is equivalent -- and the authorization lookup needs them. - agent_headers = _copilot_request_headers(request, _studio_copilot_url(workspace)) + agent_headers = _assistant_request_headers(request, _studio_assistant_url(workspace)) canonical_workspace = await _authorized_workspace(workspace, agent_headers, sid) conversation = await _get_owned_conversation( entity_store, @@ -1858,11 +1887,11 @@ async def send_message( workspace=canonical_workspace, owner_id=_request_principal_id(request), ) - agent_url = _studio_copilot_url(canonical_workspace) + agent_url = _studio_assistant_url(canonical_workspace) studio_base_url = _studio_base_url_from_request(body, request) studio_pathname = _studio_pathname_from_request(body, request) enabled_destinations = studio_links.enabled_destinations_from_request(request) - system_prompt = _build_copilot_system_prompt( + system_prompt = _build_assistant_system_prompt( sid, canonical_workspace, studio_base_url, @@ -1870,7 +1899,7 @@ async def send_message( enabled_destinations, ) return StreamingResponse( - _stream_copilot( + _stream_assistant( sid, body.message, agent_url, @@ -2002,8 +2031,8 @@ async def mcp_endpoint(session_id: str, request: Request) -> Response: { "type": "assistant", "message": { - "id": f"nemo-studio-copilot-tool-{uuid.uuid4()}", - "model": _studio_copilot_name(), + "id": f"nemo-studio-assistant-tool-{uuid.uuid4()}", + "model": _studio_assistant_name(), "content": [ { "type": "tool_use", diff --git a/services/studio/src/nmp/studio/copilot_artifacts.py b/services/studio/src/nmp/studio/assistant_artifacts.py similarity index 97% rename from services/studio/src/nmp/studio/copilot_artifacts.py rename to services/studio/src/nmp/studio/assistant_artifacts.py index 49ceefc338..2b2c1d55be 100644 --- a/services/studio/src/nmp/studio/copilot_artifacts.py +++ b/services/studio/src/nmp/studio/assistant_artifacts.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Artifact extraction for Studio copilot chat history.""" +"""Artifact extraction for Studio assistant chat history.""" import json import re @@ -9,7 +9,7 @@ from typing import Any from nmp.studio import studio_links -from pydantic import BaseModel, Field +from pydantic import AliasChoices, BaseModel, Field class ChatSelectionArtifactResponse(BaseModel): @@ -20,14 +20,14 @@ class ChatSelectionArtifactResponse(BaseModel): class ChatFileArtifactResponse(BaseModel): - """A file touched by the local copilot.""" + """A file touched by the local assistant.""" action: str path: str class ChatLinkArtifactResponse(BaseModel): - """A Studio link requested by the local copilot.""" + """A Studio link requested by the local assistant.""" label: str destination: str | None = None @@ -49,7 +49,10 @@ class ChatArtifactsResponse(BaseModel): agent: str | None = None model: str | None = None model_source: str | None = None - copilot_model: str | None = None + assistant_model: str | None = Field( + default=None, + validation_alias=AliasChoices("assistant_model", "copilot_model"), + ) workspace: str | None = None selections: list[ChatSelectionArtifactResponse] = Field(default_factory=list) files: list[ChatFileArtifactResponse] = Field(default_factory=list) @@ -124,10 +127,10 @@ def record_tool_name(artifacts: ChatArtifactsResponse, tool_name: str) -> None: _append_unique_string(artifacts.tools, tool_name) -def record_copilot_model(artifacts: ChatArtifactsResponse, model: str | None) -> None: +def record_assistant_model(artifacts: ChatArtifactsResponse, model: str | None) -> None: if not model: return - artifacts.copilot_model = model + artifacts.assistant_model = model def _set_spec_model(artifacts: ChatArtifactsResponse, model: str) -> None: diff --git a/services/studio/src/nmp/studio/copilot_mcp_tools.py b/services/studio/src/nmp/studio/assistant_mcp_tools.py similarity index 98% rename from services/studio/src/nmp/studio/copilot_mcp_tools.py rename to services/studio/src/nmp/studio/assistant_mcp_tools.py index 88212ef938..dcd31dfadd 100644 --- a/services/studio/src/nmp/studio/copilot_mcp_tools.py +++ b/services/studio/src/nmp/studio/assistant_mcp_tools.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""MCP tool catalog for Studio copilot sessions.""" +"""MCP tool catalog for Studio assistant sessions.""" from typing import Any @@ -177,9 +177,9 @@ STUDIO_LINK_TOOL_NAME, ) -STUDIO_COPILOT_CONTEXT = "\n".join( +STUDIO_ASSISTANT_CONTEXT = "\n".join( [ - "You are NeMo Copilot, running inside NeMo Studio.", + "You are NeMo Assistant, running inside NeMo Studio.", ( "NeMo Studio and the NeMo Platform API are already installed, set up, and running for this " "workspace. Treat the platform as healthy and available." diff --git a/services/studio/src/nmp/studio/copilot_skills.py b/services/studio/src/nmp/studio/assistant_skills.py similarity index 99% rename from services/studio/src/nmp/studio/copilot_skills.py rename to services/studio/src/nmp/studio/assistant_skills.py index 1bebd4c30b..b05df3d425 100644 --- a/services/studio/src/nmp/studio/copilot_skills.py +++ b/services/studio/src/nmp/studio/assistant_skills.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Claude skill discovery helpers for the Studio copilot bridge.""" +"""Claude skill discovery helpers for the Studio assistant bridge.""" import logging from collections.abc import Iterable diff --git a/services/studio/src/nmp/studio/entities.py b/services/studio/src/nmp/studio/entities.py index 42dd14b450..e496de0ba7 100644 --- a/services/studio/src/nmp/studio/entities.py +++ b/services/studio/src/nmp/studio/entities.py @@ -6,23 +6,29 @@ from typing import ClassVar, Literal from nmp.common.entities.client import EntityBase -from nmp.studio.copilot_artifacts import ChatArtifactsResponse +from nmp.studio.assistant_artifacts import ChatArtifactsResponse from pydantic import BaseModel, Field -class CopilotMessage(BaseModel): - """One user or assistant message in a persisted Copilot conversation.""" +class AssistantMessage(BaseModel): + """One user or assistant message in a persisted Assistant conversation.""" role: Literal["user", "assistant"] content: str -class CopilotConversation(EntityBase): - """A workspace-scoped, user-owned NeMo Copilot conversation.""" +class AssistantConversation(EntityBase): + """A workspace-scoped, user-owned NeMo Assistant conversation.""" - __entity_type__: ClassVar[str] = "copilot_conversation" + __entity_type__: ClassVar[str] = "assistant_conversation" session_id: str = Field(description="Stable Studio session UUID exposed to the UI.") owner_id: str = Field(description="Principal that owns and may read this conversation.") - messages: list[CopilotMessage] = Field(default_factory=list) + messages: list[AssistantMessage] = Field(default_factory=list) chat_artifacts: ChatArtifactsResponse = Field(default_factory=ChatArtifactsResponse) + + +class LegacyAssistantConversation(AssistantConversation): + """Read-compatible model for conversations persisted before the rename.""" + + __entity_type__: ClassVar[str] = "copilot_conversation" diff --git a/services/studio/src/nmp/studio/env_mappings.py b/services/studio/src/nmp/studio/env_mappings.py index f450152fd4..4b9c4f61f3 100644 --- a/services/studio/src/nmp/studio/env_mappings.py +++ b/services/studio/src/nmp/studio/env_mappings.py @@ -69,6 +69,11 @@ class EnvMapping: config_path="studio.feature_flags.base_models_enabled", default="true", ), + EnvMapping( + marker="STUDIO_UI_VITE_FF_ASSISTANT_STUDIO_ENABLED", + config_path="studio.feature_flags.assistant_studio_enabled", + default="false", + ), EnvMapping( marker="STUDIO_UI_VITE_FF_COPILOT_STUDIO_ENABLED", config_path="studio.feature_flags.copilot_studio_enabled", diff --git a/services/studio/src/nmp/studio/service.py b/services/studio/src/nmp/studio/service.py index b5054fb68c..1af3923917 100644 --- a/services/studio/src/nmp/studio/service.py +++ b/services/studio/src/nmp/studio/service.py @@ -15,7 +15,7 @@ from fastapi.responses import FileResponse, HTMLResponse from nmp.common.http_clients import shared_async_http_client from nmp.common.service import RouterConfig, Service -from nmp.studio import copilot +from nmp.studio import assistant from nmp.studio.config import StudioConfig from nmp.studio.plugins import build_plugins_router, discover_plugins from nmp.studio.static_files import SPAStaticFiles, build_csp @@ -100,7 +100,7 @@ def title(self) -> str: @property def description(self) -> str: """Service description for OpenAPI docs.""" - return "Serves the NeMo Studio web application and local copilot bridge" + return "Serves the NeMo Studio web application and local assistant bridge" def get_routers(self) -> list[RouterConfig]: """Return routers for the studio service. @@ -110,9 +110,9 @@ def get_routers(self) -> list[RouterConfig]: """ return [ RouterConfig( - copilot.router, - tag="NeMo Copilot", - description="Local copilot bridge endpoints", + assistant.router, + tag="NeMo Assistant", + description="Local assistant bridge endpoints", ) ] @@ -126,13 +126,13 @@ def configure_app(self, app: FastAPI) -> None: app: The platform's FastAPI application """ self._mount_telemetry_proxy(app) - self._mount_copilot_mcp(app) + self._mount_assistant_mcp(app) self._mount_static_files(app) self._configure_plugins(app) - def _mount_copilot_mcp(self, app: FastAPI) -> None: + def _mount_assistant_mcp(self, app: FastAPI) -> None: """Mount the auth-bypassed MCP callback before the /studio static app.""" - copilot.mount_public_mcp_route(app) + assistant.mount_public_mcp_route(app) def _get_config(self) -> StudioConfig: """Get the studio config, creating a default if none is set. diff --git a/services/studio/src/nmp/studio/studio_links.py b/services/studio/src/nmp/studio/studio_links.py index 7ca02de734..c56b39d1ec 100644 --- a/services/studio/src/nmp/studio/studio_links.py +++ b/services/studio/src/nmp/studio/studio_links.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Studio link destinations and MCP tool helpers for the copilot bridge.""" +"""Studio link destinations and MCP tool helpers for the assistant bridge.""" from collections.abc import Mapping from copy import deepcopy @@ -32,10 +32,10 @@ class StudioLinkDestination: aliases=("workspace_home", "workspace_index"), ), "dashboard": StudioLinkDestination("Workspace dashboard", "/workspaces/{workspace}/dashboard"), - "copilot": StudioLinkDestination( - "NeMo Copilot", - "/workspaces/{workspace}/dashboard/copilot", - aliases=("claude_code", "claude_code_chat", "copilot_chat"), + "assistant": StudioLinkDestination( + "NeMo Assistant", + "/workspaces/{workspace}/dashboard/assistant", + aliases=("claude_code", "claude_code_chat", "assistant_chat"), ), "agents": StudioLinkDestination( "Agents", @@ -358,7 +358,6 @@ class StudioLinkDestination: } _STUDIO_LINK_DESTINATION_FEATURE_FLAGS: dict[str, tuple[str, ...]] = { - "copilot": ("copilot_studio_enabled",), "agents": ("agents_enabled",), "agent": ("agents_enabled",), "agent_chat": ("agents_enabled",), @@ -419,7 +418,8 @@ class StudioLinkDestination: } _STUDIO_LINK_DESTINATION_ANY_FEATURE_FLAGS: dict[str, tuple[str, ...]] = { - "dashboard": ("dashboard_enabled", "copilot_studio_enabled"), + "assistant": ("assistant_studio_enabled", "copilot_studio_enabled"), + "dashboard": ("dashboard_enabled", "assistant_studio_enabled", "copilot_studio_enabled"), } _STUDIO_FEATURE_FLAG_MAPPINGS = { diff --git a/services/studio/tests/unit/test_copilot.py b/services/studio/tests/unit/test_assistant.py similarity index 80% rename from services/studio/tests/unit/test_copilot.py rename to services/studio/tests/unit/test_assistant.py index 8caadec377..a98725e9c2 100644 --- a/services/studio/tests/unit/test_copilot.py +++ b/services/studio/tests/unit/test_assistant.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Unit tests for the Studio local copilot bridge.""" +"""Unit tests for the Studio local assistant bridge.""" import asyncio import json @@ -21,19 +21,19 @@ from fastapi.testclient import TestClient from nmp.common.entities.client import EntityConflictError, EntityNotFoundError from nmp.common.service.dependencies import get_entity_client -from nmp.studio import copilot, copilot_artifacts, copilot_skills, studio_links +from nmp.studio import assistant, assistant_artifacts, assistant_skills, studio_links from nmp.studio.config import StudioConfig -from nmp.studio.entities import CopilotConversation, CopilotMessage +from nmp.studio.entities import AssistantConversation, AssistantMessage, LegacyAssistantConversation from nmp.studio.service import StudioService class FakeEntityStore: - """Small async EntityClient fake for Copilot route tests.""" + """Small async EntityClient fake for Assistant route tests.""" def __init__(self) -> None: - self.entities: dict[tuple[str, str], CopilotConversation] = {} + self.entities: dict[tuple[str, str], AssistantConversation] = {} - async def create(self, entity: CopilotConversation) -> CopilotConversation: + async def create(self, entity: AssistantConversation) -> AssistantConversation: now = datetime.now(UTC) entity._created_at = now entity._updated_at = now @@ -42,11 +42,11 @@ async def create(self, entity: CopilotConversation) -> CopilotConversation: async def get( self, - entity_type: type[CopilotConversation], + entity_type: type[AssistantConversation], name: str, *, workspace: str | None = None, - ) -> CopilotConversation: + ) -> AssistantConversation: del entity_type try: return self.entities[(workspace or "default", name)] @@ -55,7 +55,7 @@ async def get( async def list( self, - entity_type: type[CopilotConversation], + entity_type: type[AssistantConversation], *, workspace: str = "default", filter_obj: dict[str, Any] | None = None, @@ -71,14 +71,14 @@ async def list( data.sort(key=lambda entity: entity.updated_at or datetime.min.replace(tzinfo=UTC), reverse=True) return SimpleNamespace(data=data) - async def update(self, entity: CopilotConversation) -> CopilotConversation: + async def update(self, entity: AssistantConversation) -> AssistantConversation: entity._updated_at = datetime.now(UTC) self.entities[(entity.workspace, entity.name)] = entity return entity async def delete( self, - entity_type: type[CopilotConversation], + entity_type: type[AssistantConversation], name: str, *, workspace: str | None = None, @@ -92,19 +92,19 @@ async def delete( @pytest.fixture(autouse=True) -def reset_copilot_state(): +def reset_assistant_state(): """Reset module-level bridge state between tests.""" - copilot._initialized_sessions.clear() - copilot._session_streams.clear() - copilot._pending_permissions.clear() - copilot._pending_agent_inputs.clear() - copilot._session_workspace_cache.clear() + assistant._initialized_sessions.clear() + assistant._session_streams.clear() + assistant._pending_permissions.clear() + assistant._pending_agent_inputs.clear() + assistant._session_workspace_cache.clear() yield - copilot._initialized_sessions.clear() - copilot._session_streams.clear() - copilot._pending_permissions.clear() - copilot._pending_agent_inputs.clear() - copilot._session_workspace_cache.clear() + assistant._initialized_sessions.clear() + assistant._session_streams.clear() + assistant._pending_permissions.clear() + assistant._pending_agent_inputs.clear() + assistant._session_workspace_cache.clear() @pytest.fixture @@ -130,9 +130,9 @@ def service_client_with_feature_flags( def test_history_preserves_distinct_answers_under_an_agent_header(): - artifacts = copilot_artifacts.ChatArtifactsResponse() + artifacts = assistant_artifacts.ChatArtifactsResponse() question_labels_by_tool_use_id: dict[str, dict[str, str]] = {} - input_selection_tools_by_tool_use_id: dict[str, copilot_artifacts.InputSelectionTool] = {} + input_selection_tools_by_tool_use_id: dict[str, assistant_artifacts.InputSelectionTool] = {} questions = [ { "header": "Agent", @@ -146,7 +146,7 @@ def test_history_preserves_distinct_answers_under_an_agent_header(): }, ] - copilot_artifacts.record_tool_artifacts( + assistant_artifacts.record_tool_artifacts( artifacts, "AskUserQuestion", {"questions": questions}, @@ -154,7 +154,7 @@ def test_history_preserves_distinct_answers_under_an_agent_header(): question_labels_by_tool_use_id, input_selection_tools_by_tool_use_id, ) - copilot_artifacts.record_user_tool_result_artifacts( + assistant_artifacts.record_user_tool_result_artifacts( artifacts, [ { @@ -179,9 +179,9 @@ def test_history_preserves_distinct_answers_under_an_agent_header(): def test_history_records_studio_picker_results_as_selections(): - artifacts = copilot_artifacts.ChatArtifactsResponse() + artifacts = assistant_artifacts.ChatArtifactsResponse() question_labels_by_tool_use_id: dict[str, dict[str, str]] = {} - input_selection_tools_by_tool_use_id: dict[str, copilot_artifacts.InputSelectionTool] = {} + input_selection_tools_by_tool_use_id: dict[str, assistant_artifacts.InputSelectionTool] = {} picker_calls = [ ("toolu_agent", "mcp__nemo_studio__select_agent", {}, {"agent": "calculator-agent"}), ( @@ -205,7 +205,7 @@ def test_history_records_studio_picker_results_as_selections(): ] for tool_use_id, tool_name, tool_input, _result in picker_calls: - copilot_artifacts.record_tool_artifacts( + assistant_artifacts.record_tool_artifacts( artifacts, tool_name, tool_input, @@ -214,7 +214,7 @@ def test_history_records_studio_picker_results_as_selections(): input_selection_tools_by_tool_use_id, ) - copilot_artifacts.record_user_tool_result_artifacts( + assistant_artifacts.record_user_tool_result_artifacts( artifacts, [ { @@ -255,8 +255,8 @@ def _inference_source_dir(root: Path) -> Path: return source_dir -def _inference_skill(source_dir: Path) -> copilot_skills.Skill: - return copilot_skills.Skill( +def _inference_skill(source_dir: Path) -> assistant_skills.Skill: + return assistant_skills.Skill( name="inference", description="Use NeMo Platform inference.", version="0.1", @@ -287,7 +287,7 @@ def test_vendored_load_skills_from_root_loads_selected_root_without_registry_pri encoding="utf-8", ) - loaded = copilot_skills.load_skills_from_root( + loaded = assistant_skills.load_skills_from_root( tmp_path / "packages" / "nemo_platform_ext" / "skills", source_plugin="platform", source_dist="nemo-platform-ext", @@ -301,7 +301,7 @@ def test_vendored_load_skills_from_root_loads_selected_root_without_registry_pri def test_create_session_returns_uuid(service_client: TestClient): - response = service_client.post("/v2/copilot/sessions") + response = service_client.post("/v2/assistant/sessions") assert response.status_code == 200 uuid.UUID(response.json()["session_id"]) @@ -312,12 +312,12 @@ def test_create_session_persists_workspace_and_owner( entity_store: FakeEntityStore, ): response = service_client.post( - "/v2/copilot/sessions?workspace=team-a", + "/v2/assistant/sessions?workspace=team-a", headers={"X-NMP-Principal-Id": "alice@example.com"}, ) session_id = response.json()["session_id"] - persisted = entity_store.entities[("team-a", f"copilot-{session_id}")] + persisted = entity_store.entities[("team-a", f"assistant-{session_id}")] assert persisted.owner_id == "alice@example.com" assert persisted.messages == [] @@ -325,12 +325,12 @@ def test_create_session_persists_workspace_and_owner( def test_recent_conversation_messages_caps_model_context_without_mutating_history( monkeypatch: pytest.MonkeyPatch, ): - monkeypatch.setattr(copilot, "MAX_RETAINED_TURNS_PER_SESSION", 2) + monkeypatch.setattr(assistant, "MAX_RETAINED_TURNS_PER_SESSION", 2) conversation = [ - CopilotMessage(role=role, content=f"{role}-{turn}") for turn in range(3) for role in ("user", "assistant") + AssistantMessage(role=role, content=f"{role}-{turn}") for turn in range(3) for role in ("user", "assistant") ] - recent = copilot._recent_conversation_messages(conversation) + recent = assistant._recent_conversation_messages(conversation) assert [message.model_dump() for message in recent] == [ {"role": "user", "content": "user-1"}, @@ -346,21 +346,21 @@ def test_list_history_sessions_includes_persisted_conversation( entity_store: FakeEntityStore, ): session_id = str(uuid.uuid4()) - conversation = CopilotConversation( - name=f"copilot-{session_id}", + conversation = AssistantConversation( + name=f"assistant-{session_id}", workspace="default", session_id=session_id, owner_id="local-user", messages=[ - CopilotMessage(role="user", content="Help me build an agent"), - CopilotMessage(role="assistant", content="What should it do?"), + AssistantMessage(role="user", content="Help me build an agent"), + AssistantMessage(role="assistant", content="What should it do?"), ], ) conversation._created_at = datetime.fromtimestamp(40, UTC) conversation._updated_at = datetime.fromtimestamp(42, UTC) entity_store.entities[("default", conversation.name)] = conversation - response = service_client.get("/v2/copilot/history/sessions") + response = service_client.get("/v2/assistant/history/sessions") assert response.status_code == 200 assert response.json() == [ @@ -377,7 +377,7 @@ def test_list_history_sessions_includes_persisted_conversation( "agent": None, "model": None, "model_source": None, - "copilot_model": None, + "assistant_model": None, "workspace": None, "selections": [], "files": [], @@ -389,36 +389,76 @@ def test_list_history_sessions_includes_persisted_conversation( ] +def test_legacy_conversation_remains_listable_readable_and_deletable( + service_client: TestClient, + entity_store: FakeEntityStore, +): + session_id = str(uuid.uuid4()) + conversation = LegacyAssistantConversation.model_validate( + { + "name": f"copilot-{session_id}", + "workspace": "default", + "session_id": session_id, + "owner_id": "local-user", + "messages": [ + {"role": "user", "content": "Legacy prompt"}, + {"role": "assistant", "content": "Legacy answer"}, + ], + "chat_artifacts": { + "model_source": "copilot", + "copilot_model": "nvidia/legacy-model", + }, + } + ) + conversation._created_at = datetime.fromtimestamp(40, UTC) + conversation._updated_at = datetime.fromtimestamp(42, UTC) + entity_store.entities[("default", conversation.name)] = conversation + + list_response = service_client.get("/v2/assistant/history/sessions") + history_response = service_client.get(f"/v2/assistant/history/sessions/{session_id}") + delete_response = service_client.delete(f"/v2/assistant/history/sessions/{session_id}") + + assert list_response.status_code == 200 + assert list_response.json()[0]["chat_artifacts"]["assistant_model"] == "nvidia/legacy-model" + assert history_response.status_code == 200 + assert history_response.json()["items"] == [ + {"kind": "user", "text": "Legacy prompt"}, + {"kind": "assistant", "parts": [{"type": "text", "text": "Legacy answer"}]}, + ] + assert delete_response.status_code == 204 + assert ("default", conversation.name) not in entity_store.entities + + def test_history_is_scoped_to_workspace_and_owner( service_client: TestClient, entity_store: FakeEntityStore, ): alice_id = service_client.post( - "/v2/copilot/sessions?workspace=team-a", + "/v2/assistant/sessions?workspace=team-a", headers={"X-NMP-Principal-Id": "alice@example.com"}, ).json()["session_id"] bob_id = service_client.post( - "/v2/copilot/sessions?workspace=team-a", + "/v2/assistant/sessions?workspace=team-a", headers={"X-NMP-Principal-Id": "bob@example.com"}, ).json()["session_id"] - entity_store.entities[("team-a", f"copilot-{alice_id}")].messages = [ - CopilotMessage(role="user", content="Alice's private prompt"), - CopilotMessage(role="assistant", content="Alice's answer"), + entity_store.entities[("team-a", f"assistant-{alice_id}")].messages = [ + AssistantMessage(role="user", content="Alice's private prompt"), + AssistantMessage(role="assistant", content="Alice's answer"), ] - entity_store.entities[("team-a", f"copilot-{bob_id}")].messages = [ - CopilotMessage(role="user", content="Bob's private prompt"), - CopilotMessage(role="assistant", content="Bob's answer"), + entity_store.entities[("team-a", f"assistant-{bob_id}")].messages = [ + AssistantMessage(role="user", content="Bob's private prompt"), + AssistantMessage(role="assistant", content="Bob's answer"), ] response = service_client.get( - "/v2/copilot/history/sessions?workspace=team-a", + "/v2/assistant/history/sessions?workspace=team-a", headers={"X-NMP-Principal-Id": "alice@example.com"}, ) assert response.status_code == 200 assert [session["session_id"] for session in response.json()] == [alice_id] forbidden = service_client.get( - f"/v2/copilot/history/sessions/{bob_id}?workspace=team-a", + f"/v2/assistant/history/sessions/{bob_id}?workspace=team-a", headers={"X-NMP-Principal-Id": "alice@example.com"}, ) assert forbidden.status_code == 404 @@ -429,44 +469,44 @@ def test_delete_history_enforces_owner_and_removes_conversation( entity_store: FakeEntityStore, ): session_id = service_client.post( - "/v2/copilot/sessions?workspace=team-a", + "/v2/assistant/sessions?workspace=team-a", headers={"X-NMP-Principal-Id": "alice@example.com"}, ).json()["session_id"] forbidden = service_client.delete( - f"/v2/copilot/history/sessions/{session_id}?workspace=team-a", + f"/v2/assistant/history/sessions/{session_id}?workspace=team-a", headers={"X-NMP-Principal-Id": "bob@example.com"}, ) assert forbidden.status_code == 404 - assert ("team-a", f"copilot-{session_id}") in entity_store.entities + assert ("team-a", f"assistant-{session_id}") in entity_store.entities deleted = service_client.delete( - f"/v2/copilot/history/sessions/{session_id}?workspace=team-a", + f"/v2/assistant/history/sessions/{session_id}?workspace=team-a", headers={"X-NMP-Principal-Id": "alice@example.com"}, ) assert deleted.status_code == 204 - assert ("team-a", f"copilot-{session_id}") not in entity_store.entities + assert ("team-a", f"assistant-{session_id}") not in entity_store.entities def test_delete_history_rejects_active_session( service_client: TestClient, entity_store: FakeEntityStore, ): - session_id = service_client.post("/v2/copilot/sessions").json()["session_id"] - copilot._session_streams[session_id] = asyncio.Queue() + session_id = service_client.post("/v2/assistant/sessions").json()["session_id"] + assistant._session_streams[session_id] = asyncio.Queue() - response = service_client.delete(f"/v2/copilot/history/sessions/{session_id}") + response = service_client.delete(f"/v2/assistant/history/sessions/{session_id}") assert response.status_code == 409 - assert ("default", f"copilot-{session_id}") in entity_store.entities + assert ("default", f"assistant-{session_id}") in entity_store.entities -def test_copilot_turn_is_persisted_and_reused_as_context( +def test_assistant_turn_is_persisted_and_reused_as_context( service_client: TestClient, entity_store: FakeEntityStore, monkeypatch: pytest.MonkeyPatch, ): - session_id = service_client.post("/v2/copilot/sessions").json()["session_id"] + session_id = service_client.post("/v2/assistant/sessions").json()["session_id"] invocations: list[list[dict[str, str]]] = [] async def fake_invoke( @@ -478,16 +518,16 @@ async def fake_invoke( del agent_url, headers assert studio_session_id == session_id invocations.append(messages) - return f"answer-{len(invocations)}", "nvidia/copilot-model" + return f"answer-{len(invocations)}", "nvidia/assistant-model" - monkeypatch.setattr(copilot, "_invoke_copilot", fake_invoke) + monkeypatch.setattr(assistant, "_invoke_assistant", fake_invoke) first = service_client.post( - f"/v2/copilot/sessions/{session_id}/messages", + f"/v2/assistant/sessions/{session_id}/messages", json={"message": "first question", "workspace": "default"}, ) second = service_client.post( - f"/v2/copilot/sessions/{session_id}/messages", + f"/v2/assistant/sessions/{session_id}/messages", json={"message": "second question", "workspace": "default"}, ) @@ -495,7 +535,7 @@ async def fake_invoke( assert second.status_code == 200 assert "event: done" in first.text assert "event: done" in second.text - persisted = entity_store.entities[("default", f"copilot-{session_id}")] + persisted = entity_store.entities[("default", f"assistant-{session_id}")] assert [message.model_dump() for message in persisted.messages] == [ {"role": "user", "content": "first question"}, {"role": "assistant", "content": "answer-1"}, @@ -506,7 +546,7 @@ async def fake_invoke( {"role": "user", "content": "first question"}, {"role": "assistant", "content": "answer-1"}, ] - history = service_client.get(f"/v2/copilot/history/sessions/{session_id}") + history = service_client.get(f"/v2/assistant/history/sessions/{session_id}") assert [item["kind"] for item in history.json()["items"]] == [ "user", "assistant", @@ -518,36 +558,36 @@ async def fake_invoke( def test_build_claude_argv_uses_new_session_then_resume_flag(): session_id = str(uuid.uuid4()) - argv = copilot._build_claude_argv(session_id, "hello", "http://test/mcp", "Studio context") + argv = assistant._build_claude_argv(session_id, "hello", "http://test/mcp", "Studio context") assert argv[:3] == ["claude", "-p", "hello"] assert "--output-format" in argv assert "stream-json" in argv mcp_config = json.loads(argv[argv.index("--mcp-config") + 1]) - assert mcp_config["mcpServers"][copilot.CLAUDE_MCP_SERVER_NAME] == { + assert mcp_config["mcpServers"][assistant.CLAUDE_MCP_SERVER_NAME] == { "type": "http", "url": "http://test/mcp", - "timeout": copilot.CLAUDE_MCP_TOOL_TIMEOUT_MS, + "timeout": assistant.CLAUDE_MCP_TOOL_TIMEOUT_MS, } assert "--allowedTools" in argv allowed_tools = argv[argv.index("--allowedTools") + 1].split(",") - assert f"mcp__{copilot.CLAUDE_MCP_SERVER_NAME}__select_agent" in allowed_tools - assert f"mcp__{copilot.CLAUDE_MCP_SERVER_NAME}__select_eval_config" in allowed_tools - assert f"mcp__{copilot.CLAUDE_MCP_SERVER_NAME}__select_dataset_file" in allowed_tools - assert f"mcp__{copilot.CLAUDE_MCP_SERVER_NAME}__select_model" in allowed_tools - assert f"mcp__{copilot.CLAUDE_MCP_SERVER_NAME}__job_progress" in allowed_tools - assert f"mcp__{copilot.CLAUDE_MCP_SERVER_NAME}__studio_link" in allowed_tools + assert f"mcp__{assistant.CLAUDE_MCP_SERVER_NAME}__select_agent" in allowed_tools + assert f"mcp__{assistant.CLAUDE_MCP_SERVER_NAME}__select_eval_config" in allowed_tools + assert f"mcp__{assistant.CLAUDE_MCP_SERVER_NAME}__select_dataset_file" in allowed_tools + assert f"mcp__{assistant.CLAUDE_MCP_SERVER_NAME}__select_model" in allowed_tools + assert f"mcp__{assistant.CLAUDE_MCP_SERVER_NAME}__job_progress" in allowed_tools + assert f"mcp__{assistant.CLAUDE_MCP_SERVER_NAME}__studio_link" in allowed_tools assert "--disallowedTools" not in argv assert "--append-system-prompt" in argv - assert argv[argv.index("--append-system-prompt") + 1] == copilot.STUDIO_COPILOT_CONTEXT + assert argv[argv.index("--append-system-prompt") + 1] == assistant.STUDIO_ASSISTANT_CONTEXT assert "--permission-prompt-tool" in argv - assert f"mcp__{copilot.CLAUDE_MCP_SERVER_NAME}__approval_prompt" in argv + assert f"mcp__{assistant.CLAUDE_MCP_SERVER_NAME}__approval_prompt" in argv assert "--append-system-prompt" in argv assert "Studio context" in argv assert "--session-id" in argv assert session_id in argv - copilot._initialized_sessions.add(session_id) - resumed_argv = copilot._build_claude_argv(session_id, "again", "http://test/mcp", "Studio context") + assistant._initialized_sessions.add(session_id) + resumed_argv = assistant._build_claude_argv(session_id, "again", "http://test/mcp", "Studio context") assert "-r" in resumed_argv assert "--session-id" not in resumed_argv assert "--append-system-prompt" in resumed_argv @@ -564,11 +604,11 @@ def test_history_list_excludes_disk_only_sessions_but_direct_get_remains_availab project_dir.mkdir(parents=True) session_id = str(uuid.uuid4()) history = project_dir / f"{session_id}.jsonl" - first_prompt = copilot._build_claude_prompt( + first_prompt = assistant._build_claude_prompt( "first prompt", "default", "https://studio.test/studio", - "/workspaces/default/dashboard/copilot", + "/workspaces/default/dashboard/assistant", ) history.write_text( "\n".join( @@ -713,15 +753,15 @@ def test_history_list_excludes_disk_only_sessions_but_direct_get_remains_availab ) ) - monkeypatch.setattr(copilot, "SERVER_CWD", workdir) - monkeypatch.setattr(copilot, "CLAUDE_PROJECTS_DIR", projects_dir) + monkeypatch.setattr(assistant, "SERVER_CWD", workdir) + monkeypatch.setattr(assistant, "CLAUDE_PROJECTS_DIR", projects_dir) - list_response = service_client.get("/v2/copilot/history/sessions") + list_response = service_client.get("/v2/assistant/history/sessions") assert list_response.status_code == 200 assert list_response.json() == [] - history_response = service_client.get(f"/v2/copilot/history/sessions/{session_id}") + history_response = service_client.get(f"/v2/assistant/history/sessions/{session_id}") assert history_response.status_code == 200 assert history_response.json() == { @@ -803,7 +843,7 @@ def test_history_list_excludes_disk_only_sessions_but_direct_get_remains_availab "agent": "cat-identifier", "model": "cloud, nvidia/llama-3.3-nemotron-super-49b-v1", "model_source": "spec", - "copilot_model": "claude-sonnet-4-6", + "assistant_model": "claude-sonnet-4-6", "workspace": "default", "selections": [{"label": "Agent", "value": "beach-finder"}], "files": [{"action": "Wrote", "path": "agents/beach-finder.yml"}], @@ -825,7 +865,7 @@ def test_history_list_excludes_disk_only_sessions_but_direct_get_remains_availab ], }, } - assert session_id in copilot._initialized_sessions + assert session_id in assistant._initialized_sessions def test_list_claude_skills_returns_claude_install_metadata( @@ -839,10 +879,10 @@ def test_list_claude_skills_returns_claude_install_metadata( installed_skill.write_text("# Installed") skill = _inference_skill(source_dir) - monkeypatch.setattr(copilot, "SERVER_CWD", tmp_path) - monkeypatch.setattr(copilot_skills, "load_skills", lambda: {"inference": skill}) + monkeypatch.setattr(assistant, "SERVER_CWD", tmp_path) + monkeypatch.setattr(assistant_skills, "load_skills", lambda: {"inference": skill}) - response = service_client.get("/v2/copilot/skills") + response = service_client.get("/v2/assistant/skills") assert response.status_code == 200 assert response.json() == [_expected_inference_skill_response(installed=True)] @@ -884,8 +924,8 @@ def test_history_interaction_text_restores_studio_picker_submissions( expected: str, ): assert ( - copilot._history_interaction_text( - copilot.HistoryToolUse(name=tool_name, input=tool_input), + assistant._history_interaction_text( + assistant.HistoryToolUse(name=tool_name, input=tool_input), result, ) == expected @@ -900,20 +940,20 @@ def test_load_claude_skills_falls_back_on_duplicate_skill_error( skill = _inference_skill(_inference_source_dir(tmp_path)) fallback_called = False - def fallback() -> dict[str, copilot_skills.Skill]: + def fallback() -> dict[str, assistant_skills.Skill]: nonlocal fallback_called fallback_called = True return {"inference": skill} monkeypatch.setattr( - copilot_skills, + assistant_skills, "load_skills", - lambda: (_ for _ in ()).throw(copilot_skills.DuplicateSkillError("vendored drift")), + lambda: (_ for _ in ()).throw(assistant_skills.DuplicateSkillError("vendored drift")), ) - monkeypatch.setattr(copilot_skills, "_load_skills_from_preferred_entry_points", fallback) + monkeypatch.setattr(assistant_skills, "_load_skills_from_preferred_entry_points", fallback) with caplog.at_level(logging.WARNING): - loaded = copilot_skills._load_claude_skills() + loaded = assistant_skills._load_claude_skills() assert loaded == {"inference": skill} assert fallback_called @@ -925,24 +965,24 @@ def test_list_claude_skills_returns_500_when_fallback_also_fails( monkeypatch: pytest.MonkeyPatch, ): monkeypatch.setattr( - copilot_skills, + assistant_skills, "load_skills", - lambda: (_ for _ in ()).throw(copilot_skills.DuplicateSkillError("registry drift")), + lambda: (_ for _ in ()).throw(assistant_skills.DuplicateSkillError("registry drift")), ) monkeypatch.setattr( - copilot_skills, + assistant_skills, "_load_skills_from_preferred_entry_points", - lambda: (_ for _ in ()).throw(copilot_skills.DuplicateSkillError("fallback drift")), + lambda: (_ for _ in ()).throw(assistant_skills.DuplicateSkillError("fallback drift")), ) - response = service_client.get("/v2/copilot/skills") + response = service_client.get("/v2/assistant/skills") assert response.status_code == 500 assert response.json()["detail"] == "fallback drift" def test_invalid_session_id_returns_400(service_client: TestClient): - response = service_client.get("/v2/copilot/history/sessions/not-a-uuid") + response = service_client.get("/v2/assistant/history/sessions/not-a-uuid") assert response.status_code == 400 assert response.json()["detail"] == "session_id must be a UUID" @@ -954,21 +994,21 @@ async def test_stream_claude_hides_startup_oserror(monkeypatch: pytest.MonkeyPat async def fail_start(*args: Any, **kwargs: Any): raise OSError("secret local path") - monkeypatch.setattr(copilot.shutil, "which", lambda name: "/usr/bin/claude") - monkeypatch.setattr(copilot.asyncio, "create_subprocess_exec", fail_start) + monkeypatch.setattr(assistant.shutil, "which", lambda name: "/usr/bin/claude") + monkeypatch.setattr(assistant.asyncio, "create_subprocess_exec", fail_start) - chunks = [chunk async for chunk in copilot._stream_claude(session_id, "hello", "http://test/mcp")] + chunks = [chunk async for chunk in assistant._stream_claude(session_id, "hello", "http://test/mcp")] assert chunks == ['event: error\ndata: {"exit_code": null, "stderr": "Failed to start Claude Code process"}\n\n'] assert "secret local path" not in chunks[0] - assert session_id not in copilot._session_streams + assert session_id not in assistant._session_streams def test_mcp_initialize_and_tools_list(service_client: TestClient): session_id = str(uuid.uuid4()) initialize_response = service_client.post( - f"/v2/copilot/mcp/{session_id}", + f"/v2/assistant/mcp/{session_id}", json={ "jsonrpc": "2.0", "id": 1, @@ -977,7 +1017,7 @@ def test_mcp_initialize_and_tools_list(service_client: TestClient): }, ) tools_response = service_client.post( - f"/v2/copilot/mcp/{session_id}", + f"/v2/assistant/mcp/{session_id}", json={"jsonrpc": "2.0", "id": 2, "method": "tools/list"}, ) @@ -1019,7 +1059,7 @@ def test_mcp_tools_list_includes_feature_flag_enabled_destinations(monkeypatch: session_id = str(uuid.uuid4()) tools_response = service_client.post( - f"/v2/copilot/mcp/{session_id}", + f"/v2/assistant/mcp/{session_id}", json={"jsonrpc": "2.0", "id": 2, "method": "tools/list"}, ) @@ -1032,11 +1072,19 @@ def test_mcp_tools_list_includes_feature_flag_enabled_destinations(monkeypatch: assert "chat with or try a model" in studio_link_tool["description"] +@pytest.mark.parametrize("feature_flag", ["assistant_studio_enabled", "copilot_studio_enabled"]) +def test_assistant_destinations_accept_current_and_legacy_feature_flags(feature_flag: str): + enabled_destinations = studio_links.enabled_destinations({feature_flag: True}) + + assert "assistant" in enabled_destinations + assert "dashboard" in enabled_destinations + + def test_build_studio_system_prompt_preserves_empty_enabled_destinations(): - prompt = copilot._build_studio_system_prompt( + prompt = assistant._build_studio_system_prompt( "default", "https://studio.test", - "/workspaces/default/dashboard/copilot", + "/workspaces/default/dashboard/assistant", {}, ) @@ -1047,16 +1095,16 @@ def test_build_studio_system_prompt_preserves_empty_enabled_destinations(): def test_build_studio_system_prompt_includes_message_summary_contract(): - prompt = copilot._build_studio_system_prompt( + prompt = assistant._build_studio_system_prompt( "default", "https://studio.test", - "/workspaces/default/dashboard/copilot", + "/workspaces/default/dashboard/assistant", {}, ) assert "Conditional message-summary behavior:" in prompt - assert copilot.STUDIO_MESSAGE_SUMMARY_START in prompt - assert copilot.STUDIO_MESSAGE_SUMMARY_END in prompt + assert assistant.STUDIO_MESSAGE_SUMMARY_START in prompt + assert assistant.STUDIO_MESSAGE_SUMMARY_END in prompt assert "title: " in prompt assert "summary: None: super().__init__() self.update_calls = 0 - async def update(self, entity: CopilotConversation) -> CopilotConversation: + async def update(self, entity: AssistantConversation) -> AssistantConversation: self.update_calls += 1 if self.update_calls == 1: self.entities[(concurrent_conversation.workspace, concurrent_conversation.name)] = ( @@ -2374,11 +2422,11 @@ async def update(self, entity: CopilotConversation) -> CopilotConversation: async def fake_invoke(agent_url, headers, messages, studio_session_id): return "local answer", "model-x" - monkeypatch.setattr(copilot, "_invoke_copilot", fake_invoke) + monkeypatch.setattr(assistant, "_invoke_assistant", fake_invoke) frames = [ frame - async for frame in copilot._stream_copilot( + async for frame in assistant._stream_assistant( session_id, "local question", "https://agent.test/x", @@ -2398,14 +2446,14 @@ async def fake_invoke(agent_url, headers, messages, studio_session_id): "local question", "local answer", ] - assert persisted.chat_artifacts.copilot_model == "model-x" + assert persisted.chat_artifacts.assistant_model == "model-x" -def test_copilot_request_payload_keeps_session_outside_model_messages(): +def test_assistant_request_payload_keeps_session_outside_model_messages(): messages = [{"role": "user", "content": "hello"}] session_id = str(uuid.uuid4()) - payload = copilot._copilot_request_payload(messages, session_id) + payload = assistant._assistant_request_payload(messages, session_id) assert payload == { "messages": messages, @@ -2423,11 +2471,11 @@ def test_public_mcp_route_is_mounted_before_static_fallback(): session_id = str(uuid.uuid4()) response = client.post( - f"/studio/api/copilot/mcp/{session_id}", + f"/studio/api/assistant/mcp/{session_id}", json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"}, ) - get_response = client.get(f"/studio/api/copilot/mcp/{session_id}") - delete_response = client.delete(f"/studio/api/copilot/mcp/{session_id}") + get_response = client.get(f"/studio/api/assistant/mcp/{session_id}") + delete_response = client.delete(f"/studio/api/assistant/mcp/{session_id}") assert response.status_code == 200 assert [tool["name"] for tool in response.json()["result"]["tools"]] == [ @@ -2445,10 +2493,10 @@ def test_public_mcp_route_is_mounted_before_static_fallback(): assert delete_response.headers["allow"] == "POST" -def test_copilot_routes_are_available_by_default(): +def test_assistant_routes_are_available_by_default(): client = TestClient(StudioService().app) - response = client.post("/v2/copilot/sessions") + response = client.post("/v2/assistant/sessions") assert response.status_code == 200 uuid.UUID(response.json()["session_id"]) @@ -2456,30 +2504,30 @@ def test_copilot_routes_are_available_by_default(): def test_parse_reasoning_step_output_extracts_the_trace(): payload = "**Input:**\n```python\nchain of thought\n```\n\n**Output:** The user asked a math question." - assert copilot._parse_reasoning_step_output(payload) == "The user asked a math question." + assert assistant._parse_reasoning_step_output(payload) == "The user asked a math question." def test_parse_reasoning_step_output_ignores_a_start_step(): # The paired start step has no Output block yet. - assert copilot._parse_reasoning_step_output("**Input:**\n```python\nchain of thought\n```") == "" - assert copilot._parse_reasoning_step_output(None) == "" + assert assistant._parse_reasoning_step_output("**Input:**\n```python\nchain of thought\n```") == "" + assert assistant._parse_reasoning_step_output(None) == "" def test_reasoning_stream_event_shape(): - event_type, payload = copilot._reasoning_stream_event("thinking out loud") + event_type, payload = assistant._reasoning_stream_event("thinking out loud") assert event_type == "agent" block = json.loads(payload)["message"]["content"][0] assert block == {"type": "reasoning", "text": "thinking out loud"} @pytest.mark.asyncio -async def test_invoke_copilot_relays_reasoning_despite_shared_step_id(monkeypatch: pytest.MonkeyPatch): +async def test_invoke_assistant_relays_reasoning_despite_shared_step_id(monkeypatch: pytest.MonkeyPatch): """A reasoning step is a start/end pair sharing one id. The tool dedup must not claim that id on the start, or the end -- which carries the trace -- is dropped. """ session_id = str(uuid.uuid4()) queue: asyncio.Queue = asyncio.Queue() - copilot._session_streams[session_id] = queue + assistant._session_streams[session_id] = queue start = json.dumps({"id": "step-1", "name": "Reasoning: model", "payload": "**Input:**\n```python\nx\n```"}) end = json.dumps( @@ -2523,9 +2571,9 @@ async def __aexit__(self, *args): def stream(self, *args, **kwargs): return _Stream() - monkeypatch.setattr(copilot.httpx, "AsyncClient", lambda **kwargs: _Client()) + monkeypatch.setattr(assistant.httpx, "AsyncClient", lambda **kwargs: _Client()) - text, _ = await copilot._invoke_copilot("https://a.test", {}, [], session_id) + text, _ = await assistant._invoke_assistant("https://a.test", {}, [], session_id) assert text == "done" events = [] @@ -2537,12 +2585,12 @@ def stream(self, *args, **kwargs): @pytest.mark.asyncio -async def test_invoke_copilot_emits_a_repeated_reasoning_step_once(monkeypatch: pytest.MonkeyPatch): +async def test_invoke_assistant_emits_a_repeated_reasoning_step_once(monkeypatch: pytest.MonkeyPatch): """A repeated completed step must not render the same trace twice, while the start of the pair -- which shares its id -- must never claim that id.""" session_id = str(uuid.uuid4()) queue: asyncio.Queue = asyncio.Queue() - copilot._session_streams[session_id] = queue + assistant._session_streams[session_id] = queue start = json.dumps({"id": "step-1", "name": "Reasoning: model", "payload": "**Input:**\n```python\nx\n```"}) end = json.dumps( @@ -2587,9 +2635,9 @@ async def __aexit__(self, *args): def stream(self, *args, **kwargs): return _Stream() - monkeypatch.setattr(copilot.httpx, "AsyncClient", lambda **kwargs: _Client()) + monkeypatch.setattr(assistant.httpx, "AsyncClient", lambda **kwargs: _Client()) - await copilot._invoke_copilot("https://a.test", {}, [], session_id) + await assistant._invoke_assistant("https://a.test", {}, [], session_id) blocks = [] while not queue.empty(): diff --git a/services/studio/tests/unit/test_service.py b/services/studio/tests/unit/test_service.py index b2b15ac147..2e9d6237c8 100644 --- a/services/studio/tests/unit/test_service.py +++ b/services/studio/tests/unit/test_service.py @@ -54,14 +54,14 @@ def test_service_title(self): def test_service_description(self): """Test that the service has the correct description.""" service = StudioService() - assert service.description == "Serves the NeMo Studio web application and local copilot bridge" + assert service.description == "Serves the NeMo Studio web application and local assistant bridge" - def test_get_routers_returns_copilot_router(self): - """Test that the service exposes the local copilot API router.""" + def test_get_routers_returns_assistant_router(self): + """Test that the service exposes the local assistant API router.""" service = StudioService() routers = service.get_routers() assert len(routers) == 1 - assert routers[0].tag == "NeMo Copilot" + assert routers[0].tag == "NeMo Assistant" def test_module_name(self): """Test that the service has the correct module name.""" diff --git a/web/.prettierignore b/web/.prettierignore index 4bae88a8cd..d86eebcc51 100644 --- a/web/.prettierignore +++ b/web/.prettierignore @@ -7,9 +7,10 @@ pnpm-lock.yaml # Build output dist/ packages/*/dist/ +packages/studio/public/vendor/ packages/**/.test-reports/ packages/**/test-results/ packages/**/playwright-report/ # Ignore generated style -packages/studio/src/generated/* \ No newline at end of file +packages/studio/src/generated/* diff --git a/web/packages/studio/env/.env.dev.local.sample b/web/packages/studio/env/.env.dev.local.sample index 847320928d..97ee4c9b2b 100644 --- a/web/packages/studio/env/.env.dev.local.sample +++ b/web/packages/studio/env/.env.dev.local.sample @@ -23,6 +23,7 @@ VITE_AUTH_SCOPE_PREFIX="api://" VITE_FF_AGENTS_ENABLED='true' VITE_FF_ANONYMIZER_ENABLED='false' VITE_FF_BASE_MODELS_ENABLED='true' +VITE_FF_ASSISTANT_STUDIO_ENABLED=false VITE_FF_COPILOT_STUDIO_ENABLED=false VITE_FF_CUSTOMIZER_ENABLED='true' VITE_FF_DASHBOARD_ENABLED='false' diff --git a/web/packages/studio/env/.env.fastapi b/web/packages/studio/env/.env.fastapi index 59c2a5927a..c0b97bfb32 100644 --- a/web/packages/studio/env/.env.fastapi +++ b/web/packages/studio/env/.env.fastapi @@ -21,6 +21,7 @@ VITE_TELEMETRY_ENABLED=STUDIO_UI_VITE_TELEMETRY_ENABLED VITE_FF_AGENTS_ENABLED=STUDIO_UI_VITE_FF_AGENTS_ENABLED VITE_FF_ANONYMIZER_ENABLED=STUDIO_UI_VITE_FF_ANONYMIZER_ENABLED VITE_FF_BASE_MODELS_ENABLED=STUDIO_UI_VITE_FF_BASE_MODELS_ENABLED +VITE_FF_ASSISTANT_STUDIO_ENABLED=STUDIO_UI_VITE_FF_ASSISTANT_STUDIO_ENABLED VITE_FF_COPILOT_STUDIO_ENABLED=STUDIO_UI_VITE_FF_COPILOT_STUDIO_ENABLED VITE_FF_CUSTOMIZER_ENABLED=STUDIO_UI_VITE_FF_CUSTOMIZER_ENABLED VITE_FF_DASHBOARD_ENABLED=STUDIO_UI_VITE_FF_DASHBOARD_ENABLED diff --git a/web/packages/studio/src/components/CustomizeModelModal/constants.ts b/web/packages/studio/src/components/CustomizeModelModal/constants.ts index abd08f9cec..867d6f1a2f 100644 --- a/web/packages/studio/src/components/CustomizeModelModal/constants.ts +++ b/web/packages/studio/src/components/CustomizeModelModal/constants.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -// Fine-tuning is now run through the NeMo Copilot (guided); prompt-tuning uses the in-app form. +// Fine-tuning is now run through the NeMo Assistant (guided); prompt-tuning uses the in-app form. export type CustomizationMethod = 'fine-tuned' | 'prompt-tuned'; export interface CustomizationMethodOption { @@ -20,7 +20,7 @@ export const CUSTOMIZATION_METHODS: CustomizationMethodOption[] = [ tags: ['SFT', 'LoRA'], tagColor: 'purple', description: - 'Highest accuracy. Opens the NeMo Copilot to guide you through configuring and launching a fine-tuning job.', + 'Highest accuracy. Opens the NeMo Assistant to guide you through configuring and launching a fine-tuning job.', bestFor: 'Domain adaptation, strict output control, or minimizing error rates.', }, { diff --git a/web/packages/studio/src/components/ExperimentCreateModal/index.tsx b/web/packages/studio/src/components/ExperimentCreateModal/index.tsx index 7c72ac1ffe..e200bf4627 100644 --- a/web/packages/studio/src/components/ExperimentCreateModal/index.tsx +++ b/web/packages/studio/src/components/ExperimentCreateModal/index.tsx @@ -137,7 +137,7 @@ export const ExperimentCreateModal: FC = ({ Create experiment - NeMo Copilot + NeMo Assistant CLI command @@ -180,7 +180,7 @@ export const ExperimentCreateModal: FC = ({ - + ({ ThemeSwitch: () =>
, })); -vi.mock('@studio/routes/agents/CopilotChatRoute/CopilotTopBarChat', () => ({ - CopilotTopBarChat: () =>
, +vi.mock('@studio/routes/agents/AssistantChatRoute/AssistantTopBarChat', () => ({ + AssistantTopBarChat: () =>
, })); vi.mock('@studio/constants/environment', async (importOriginal) => { @@ -89,30 +89,30 @@ describe('GlobalNav', () => { expect(screen.getByText('NeMo Studio')).toBeInTheDocument(); }); - it('starts collapsed on the NeMo Copilot route when no preference is saved', async () => { + it('starts collapsed on the NeMo Assistant route when no preference is saved', async () => { createMatchMediaMock(true); await renderGlobalNav( - generatePath(ROUTES.workspace.copilotChat, { workspace: 'test-workspace' }) + generatePath(ROUTES.workspace.assistantChat, { workspace: 'test-workspace' }) ); expect(screen.getByRole('button', { name: 'Expand sidebar' })).toBeInTheDocument(); expect(screen.queryByText('NeMo Studio')).not.toBeInTheDocument(); }); - it('respects a saved expanded preference on the NeMo Copilot route', async () => { + it('respects a saved expanded preference on the NeMo Assistant route', async () => { localStorage.setItem(SIDE_NAV_OPEN_KEY, JSON.stringify('true')); createMatchMediaMock(true); await renderGlobalNav( - generatePath(ROUTES.workspace.copilotChat, { workspace: 'test-workspace' }) + generatePath(ROUTES.workspace.assistantChat, { workspace: 'test-workspace' }) ); expect(screen.getByRole('button', { name: 'Collapse sidebar' })).toBeInTheDocument(); expect(screen.getByText('NeMo Studio')).toBeInTheDocument(); }); - it('uses the NeMo Copilot default during in-app navigation', async () => { + it('uses the NeMo Assistant default during in-app navigation', async () => { createMatchMediaMock(true); const { GlobalNav } = await import('@studio/components/Layouts/GlobalNav/index'); const router = createMemoryRouter( @@ -132,7 +132,7 @@ describe('GlobalNav', () => { await act(async () => { await router.navigate( - generatePath(ROUTES.workspace.copilotChat, { workspace: 'test-workspace' }) + generatePath(ROUTES.workspace.assistantChat, { workspace: 'test-workspace' }) ); }); @@ -234,13 +234,13 @@ describe('GlobalNav', () => { }); }); - describe('NeMo Copilot top bar chat', () => { - it('mounts outside the dashboard and full NeMo Copilot routes', async () => { + describe('NeMo Assistant top bar chat', () => { + it('mounts outside the dashboard and full NeMo Assistant routes', async () => { createMatchMediaMock(true); await renderGlobalNav('/workspaces/test-workspace/jobs'); - expect(screen.getByTestId('copilot-top-bar-chat')).toBeInTheDocument(); + expect(screen.getByTestId('assistant-top-bar-chat')).toBeInTheDocument(); }); it('does not mount on the dashboard route', async () => { @@ -250,17 +250,17 @@ describe('GlobalNav', () => { generatePath(ROUTES.workspace.dashboard, { workspace: 'test-workspace' }) ); - expect(screen.queryByTestId('copilot-top-bar-chat')).not.toBeInTheDocument(); + expect(screen.queryByTestId('assistant-top-bar-chat')).not.toBeInTheDocument(); }); - it('does not mount on the full NeMo Copilot route', async () => { + it('does not mount on the full NeMo Assistant route', async () => { createMatchMediaMock(true); await renderGlobalNav( - generatePath(ROUTES.workspace.copilotChat, { workspace: 'test-workspace' }) + generatePath(ROUTES.workspace.assistantChat, { workspace: 'test-workspace' }) ); - expect(screen.queryByTestId('copilot-top-bar-chat')).not.toBeInTheDocument(); + expect(screen.queryByTestId('assistant-top-bar-chat')).not.toBeInTheDocument(); }); }); }); diff --git a/web/packages/studio/src/components/Layouts/GlobalNav/index.tsx b/web/packages/studio/src/components/Layouts/GlobalNav/index.tsx index 3550621901..3eb00054fc 100644 --- a/web/packages/studio/src/components/Layouts/GlobalNav/index.tsx +++ b/web/packages/studio/src/components/Layouts/GlobalNav/index.tsx @@ -7,7 +7,7 @@ import { UserPopover } from '@studio/components/UserPopover'; import { TOUR_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { useWorkspaceFromPathIfExists } from '@studio/hooks/useWorkspaceFromPath'; -import { CopilotTopBarChat } from '@studio/routes/agents/CopilotChatRoute/CopilotTopBarChat'; +import { AssistantTopBarChat } from '@studio/routes/agents/AssistantChatRoute/AssistantTopBarChat'; import { ThemeSwitch } from '@studio/routes/PageLayout/ThemeSwitch'; import { getWorkspaceDetailsDefaultRoute } from '@studio/routes/utils'; import { useSidebarState } from '@studio/util/hooks/useSidebarState'; @@ -25,18 +25,18 @@ interface Props { interface GlobalNavContentProps extends Props { isDashboardRoute: boolean; - isCopilotChatRoute: boolean; + isAssistantChatRoute: boolean; } const GlobalNavContent: FC = ({ sideNav, isDashboardRoute, - isCopilotChatRoute, + isAssistantChatRoute, }) => { const workspace = useWorkspaceFromPathIfExists(); - const { expanded, toggle } = useSidebarState(!isCopilotChatRoute); - const shouldMountCopilotTopBarChat = !isDashboardRoute && !isCopilotChatRoute; - const sidebarBackground = isCopilotChatRoute + const { expanded, toggle } = useSidebarState(!isAssistantChatRoute); + const shouldMountAssistantTopBarChat = !isDashboardRoute && !isAssistantChatRoute; + const sidebarBackground = isAssistantChatRoute ? 'bg-surface-sunken dark:bg-surface-base' : 'bg-surface-navigation'; @@ -86,7 +86,7 @@ const GlobalNavContent: FC = ({ )} - {shouldMountCopilotTopBarChat && } + {shouldMountAssistantTopBarChat && } @@ -96,7 +96,7 @@ const GlobalNavContent: FC = ({ /> {sideNav && (
{sideNav(!expanded)} @@ -110,15 +110,15 @@ export const GlobalNav: FC = ({ sideNav }) => { const location = useLocation(); const isDashboardRoute = matchPath({ path: ROUTES.workspace.dashboard, end: true }, location.pathname) !== null; - const isCopilotChatRoute = - matchPath({ path: ROUTES.workspace.copilotChat, end: true }, location.pathname) !== null; + const isAssistantChatRoute = + matchPath({ path: ROUTES.workspace.assistantChat, end: true }, location.pathname) !== null; return ( ); }; diff --git a/web/packages/studio/src/components/dataViews/EvaluationSessionsDataView/Empty.tsx b/web/packages/studio/src/components/dataViews/EvaluationSessionsDataView/Empty.tsx index d85553d626..5ed32c9b14 100644 --- a/web/packages/studio/src/components/dataViews/EvaluationSessionsDataView/Empty.tsx +++ b/web/packages/studio/src/components/dataViews/EvaluationSessionsDataView/Empty.tsx @@ -35,9 +35,9 @@ export const Empty = ({ experimentName, datasetName }: EmptyProps) => {
- + - NeMo Copilot + NeMo Assistant @@ -45,7 +45,7 @@ export const Empty = ({ experimentName, datasetName }: EmptyProps) => {
- + {
- + - NeMo Copilot + NeMo Assistant @@ -45,7 +45,7 @@ export const Empty = ({ experimentName }: EmptyProps) => {
- + = ({
); diff --git a/web/packages/studio/src/routes/DashboardLandingRoute/index.test.tsx b/web/packages/studio/src/routes/DashboardLandingRoute/index.test.tsx index 42d91bb156..15ce0fca21 100644 --- a/web/packages/studio/src/routes/DashboardLandingRoute/index.test.tsx +++ b/web/packages/studio/src/routes/DashboardLandingRoute/index.test.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { ROUTES } from '@studio/constants/routes'; -import { getCopilotActiveSessionStorageKey } from '@studio/routes/agents/CopilotChatRoute/activeSessionStorage'; +import { getAssistantActiveSessionStorageKey } from '@studio/routes/agents/AssistantChatRoute/activeSessionStorage'; import { DashboardLandingRoute } from '@studio/routes/DashboardLandingRoute'; import { mockFeatureFlags } from '@studio/tests/util/mockFeatureFlags'; import { TestProviders } from '@studio/tests/util/TestProviders'; @@ -10,13 +10,13 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { createMemoryRouter, generatePath, RouterProvider, useLocation } from 'react-router'; -vi.mock('@studio/routes/agents/CopilotChatRoute/api', async (importOriginal) => { +vi.mock('@studio/routes/agents/AssistantChatRoute/api', async (importOriginal) => { const actual = - await importOriginal(); + await importOriginal(); return { ...actual, - listCopilotHistorySessions: vi.fn(async () => []), + listAssistantHistorySessions: vi.fn(async () => []), }; }); @@ -39,7 +39,7 @@ const renderRoute = () => { const router = createMemoryRouter( [ { path: ROUTES.workspace.dashboard, element: }, - { path: ROUTES.workspace.copilotChat, element: }, + { path: ROUTES.workspace.assistantChat, element: }, ], { initialEntries: [route], @@ -73,7 +73,7 @@ describe('DashboardLandingRoute', () => { renderRoute(); expect(await screen.findByText('What would you like to do?')).toBeInTheDocument(); - const composer = screen.getByRole('textbox', { name: 'Message NeMo Copilot' }); + const composer = screen.getByRole('textbox', { name: 'Message NeMo Assistant' }); expect(composer).toBeInTheDocument(); expect(screen.getByTestId('dashboard-landing-composer')).toHaveClass('rounded-lg'); expect(screen.getByTestId('dashboard-landing-composer')).not.toHaveClass('rounded-2xl'); @@ -92,7 +92,7 @@ describe('DashboardLandingRoute', () => { const user = userEvent.setup(); renderRoute(); - const composer = await screen.findByRole('textbox', { name: 'Message NeMo Copilot' }); + const composer = await screen.findByRole('textbox', { name: 'Message NeMo Assistant' }); const sendButton = screen.getByRole('button', { name: 'Send message' }); expect(sendButton).toBeDisabled(); @@ -104,33 +104,33 @@ describe('DashboardLandingRoute', () => { }); }); - it('navigates to the NeMo Copilot chat with the submitted prompt', async () => { + it('navigates to the NeMo Assistant chat with the submitted prompt', async () => { const user = userEvent.setup(); renderRoute(); await user.type( - await screen.findByRole('textbox', { name: 'Message NeMo Copilot' }), + await screen.findByRole('textbox', { name: 'Message NeMo Assistant' }), 'Check repo' ); await user.click(screen.getByRole('button', { name: 'Send message' })); expect(await screen.findByTestId(CHAT_ROUTE_TEST_ID)).toHaveTextContent( - `${generatePath(ROUTES.workspace.copilotChat, { workspace })}|Check repo` + `${generatePath(ROUTES.workspace.assistantChat, { workspace })}|Check repo` ); }); - it('clears the active NeMo Copilot session before starting from the landing composer', async () => { + it('clears the active NeMo Assistant session before starting from the landing composer', async () => { const user = userEvent.setup(); - localStorage.setItem(getCopilotActiveSessionStorageKey(workspace), 'session-existing'); + localStorage.setItem(getAssistantActiveSessionStorageKey(workspace), 'session-existing'); renderRoute(); await user.type( - await screen.findByRole('textbox', { name: 'Message NeMo Copilot' }), + await screen.findByRole('textbox', { name: 'Message NeMo Assistant' }), 'Check repo' ); await user.click(screen.getByRole('button', { name: 'Send message' })); - expect(localStorage.getItem(getCopilotActiveSessionStorageKey(workspace))).toBeNull(); + expect(localStorage.getItem(getAssistantActiveSessionStorageKey(workspace))).toBeNull(); }); it('submits the landing composer when Enter is pressed', async () => { @@ -138,13 +138,13 @@ describe('DashboardLandingRoute', () => { renderRoute(); await user.type( - await screen.findByRole('textbox', { name: 'Message NeMo Copilot' }), + await screen.findByRole('textbox', { name: 'Message NeMo Assistant' }), 'Check repo' ); await user.keyboard('{Enter}'); expect(await screen.findByTestId(CHAT_ROUTE_TEST_ID)).toHaveTextContent( - `${generatePath(ROUTES.workspace.copilotChat, { workspace })}|Check repo` + `${generatePath(ROUTES.workspace.assistantChat, { workspace })}|Check repo` ); }); @@ -152,7 +152,7 @@ describe('DashboardLandingRoute', () => { const user = userEvent.setup(); renderRoute(); - const composer = await screen.findByRole('textbox', { name: 'Message NeMo Copilot' }); + const composer = await screen.findByRole('textbox', { name: 'Message NeMo Assistant' }); await user.type(composer, 'Line one'); await user.keyboard('{Shift>}{Enter}{/Shift}'); diff --git a/web/packages/studio/src/routes/DashboardLandingRoute/index.tsx b/web/packages/studio/src/routes/DashboardLandingRoute/index.tsx index 9418c9c2fa..dae76f9d09 100644 --- a/web/packages/studio/src/routes/DashboardLandingRoute/index.tsx +++ b/web/packages/studio/src/routes/DashboardLandingRoute/index.tsx @@ -6,10 +6,10 @@ import { GradientBackground } from '@nemo/common/src/components/GradientBackgrou import { Button, Flex, Text, TextArea, Tooltip } from '@nvidia/foundations-react-core'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { useBreadcrumbs } from '@studio/providers/breadcrumbs/useBreadcrumbs'; -import { writeStoredActiveSessionId } from '@studio/routes/agents/CopilotChatRoute/activeSessionStorage'; -import { CopilotLayout } from '@studio/routes/agents/CopilotChatRoute/CopilotLayout'; -import type { CopilotChatRouteState } from '@studio/routes/agents/CopilotChatRoute/types'; -import { getCopilotChatRoute } from '@studio/routes/utils'; +import { writeStoredActiveSessionId } from '@studio/routes/agents/AssistantChatRoute/activeSessionStorage'; +import { AssistantLayout } from '@studio/routes/agents/AssistantChatRoute/AssistantLayout'; +import type { AssistantChatRouteState } from '@studio/routes/agents/AssistantChatRoute/types'; +import { getAssistantChatRoute } from '@studio/routes/utils'; import { Send, Terminal } from 'lucide-react'; import { type ChangeEvent, @@ -57,10 +57,10 @@ const LandingComposer = ({ data-tour="dashboard-get-started" >