diff --git a/plugins/memory/mem0/_setup.py b/plugins/memory/mem0/_setup.py index 17a41300a0..3120b0dbad 100644 --- a/plugins/memory/mem0/_setup.py +++ b/plugins/memory/mem0/_setup.py @@ -448,8 +448,8 @@ def _install_provider_deps(llm_id: str, embedder_id: str, vector_id: str) -> Non missing.append(dep) if missing: print("\n The selected backends need extra packages:") - print(f" uv pip install {' '.join(missing)}") - print(" Run that inside the hermes venv, then re-run setup.") + print(f" Missing: {', '.join(missing)}") + print(" Declare these requirements in the plugin's pyproject.toml, then run `hermes pm install` and restart Hermes.") def _probe(fn, ok: str, fail: str, exc=Exception) -> tuple[bool, str]: @@ -510,7 +510,7 @@ def post_setup(hermes_home: str, config: dict) -> None: import mem0 installed_ver = getattr(mem0, "__version__", None) if installed_ver and tuple(int(x) for x in installed_ver.split(".")[:3]) < (2, 0, 7): - print(f"\n ⚠ mem0ai {installed_ver} installed but >=2.0.7 required.\n Run: uv pip install --python {sys.executable} 'mem0ai>=2.0.7'") + print(f"\n ⚠ mem0ai {installed_ver} installed but >=2.0.7 required.\n Run `hermes pm repair`, then restart Hermes.") flags = parse_flags(sys.argv[1:]) handler = _MODE_HANDLERS.get(flags["mode"]) flags["_mode_from_flag"] = handler is not None diff --git a/plugins/platforms/teams/adapter.py b/plugins/platforms/teams/adapter.py index 7b70f82489..13a52b9e02 100644 --- a/plugins/platforms/teams/adapter.py +++ b/plugins/platforms/teams/adapter.py @@ -787,19 +787,8 @@ def interactive_setup() -> None: def _install_hint() -> str: - """Build the Teams install hint string. - - Prefers ``uv sync --frozen --extra teams`` (respects pyproject pinning); - falls back to a plain pip install of the two packages if that is - unavailable. Restarting the gateway also auto-installs via pm. - """ - try: - cmd = "uv sync --frozen --extra teams" - except Exception: # pragma: no cover — defensive - cmd = None - if not cmd: - cmd = f"{sys.executable} -m pip install microsoft-teams-apps aiohttp" - return f"Teams SDK missing — restart the gateway to auto-install, or run: {cmd}" + """Point to the setup flow that requests PM's declared Teams extra.""" + return "Teams SDK missing — run `hermes setup`, configure Teams, then restart the gateway" def register(ctx) -> None: diff --git a/pm/build_env.py b/pm/build_env.py index 649b79bda4..2bf34738c2 100644 --- a/pm/build_env.py +++ b/pm/build_env.py @@ -17,6 +17,7 @@ def main(argv: Sequence[str] | None = None) -> int: parser.add_argument("--cache", type=Path) parser.add_argument("--extra", dest="extras", action="append", default=[]) parser.add_argument("--group", dest="groups", action="append", default=[]) + parser.add_argument("--only-groups", action="store_true", help="install selected groups without the application") parser.add_argument("--all-extras", action="store_true") parser.add_argument("--no-install-project", action="store_true") parser.add_argument("--resolve", action="store_true", help="resolve the source lock before building") @@ -77,7 +78,7 @@ def main(argv: Sequence[str] | None = None) -> int: else: executable = pm.build_environment( source=args.source, out=args.out, python=args.python, cache=args.cache, - extras=args.extras, groups=args.groups, all_extras=args.all_extras, + extras=args.extras, groups=args.groups, only_groups=args.only_groups, all_extras=args.all_extras, no_install_project=args.no_install_project, frozen=not args.resolve, sealed=args.sealed, offline=args.offline, explicit=True, ) diff --git a/pm/client.py b/pm/client.py index 208254bebb..3eb1db05ce 100644 --- a/pm/client.py +++ b/pm/client.py @@ -237,7 +237,7 @@ def _python_operation(operation: str, arguments: dict): def build_environment( *, source: Path, out: Path, python: Path | None = None, cache: Path | None = None, env: Mapping[str, str] | None = None, - extras: Sequence[str] = (), groups: Sequence[str] = (), + extras: Sequence[str] = (), groups: Sequence[str] = (), only_groups: bool = False, all_extras: bool = False, no_install_project: bool = False, frozen: bool = True, sealed: bool = False, offline: bool = False, explicit: bool = False, timeout: int = 1800, @@ -245,7 +245,7 @@ def build_environment( """Build a validated Python environment without exposing install machinery.""" return Path(_python_operation("build_environment", { "source": Path(source), "out": Path(out), "python": python, "cache": cache, - "env": dict(env) if env is not None else None, "extras": list(extras), "groups": list(groups), + "env": dict(env) if env is not None else None, "extras": list(extras), "groups": list(groups), "only_groups": only_groups, "all_extras": all_extras, "no_install_project": no_install_project, "frozen": frozen, "sealed": sealed, "offline": offline, "explicit": explicit, "timeout": timeout, diff --git a/pm/environment.py b/pm/environment.py index 01c7fa98cf..55c4eb2293 100644 --- a/pm/environment.py +++ b/pm/environment.py @@ -188,7 +188,7 @@ class PythonEnvironment: def sync(self, source: Path, *, extras: Sequence[str] = (), groups: Sequence[str] = (), timeout: int = 1800, frozen: bool = True, all_extras: bool = False, no_install_project: bool = False, locked: bool = False, - no_default_groups: bool = False) -> None: + no_default_groups: bool = False, only_groups: bool = False) -> None: """Install the root and every member; resolve only in a writable workspace. ``frozen=False`` is reserved for the caller-owned generated workspace, @@ -196,6 +196,8 @@ class PythonEnvironment: """ from pm.workspace import classify_uv_failure + if only_groups and (not groups or extras or all_extras): + raise ValueError("group-only builds require groups and cannot select extras") if not frozen: self.lock(source, timeout=timeout) # Locking members alone is insufficient: plain sync only installs root deps. @@ -216,7 +218,7 @@ class PythonEnvironment: for extra in sorted(set(extras)): command += ["--extra", extra] for group in sorted(set(groups)): - command += ["--group", group] + command += ["--only-group" if only_groups else "--group", group] result = self._run(command, cwd=source, timeout=timeout) if result.returncode: raise classify_uv_failure("sync", result.returncode, result.stderr or result.stdout) diff --git a/pm/operations.py b/pm/operations.py index b07358e7a4..a857d3c99f 100644 --- a/pm/operations.py +++ b/pm/operations.py @@ -28,7 +28,7 @@ def _require_install_allowed(explicit: bool) -> None: def build_environment( *, source: Path, out: Path, python: Path | None = None, cache: Path | None = None, env: Mapping[str, str] | None = None, - extras: Sequence[str] = (), groups: Sequence[str] = (), + extras: Sequence[str] = (), groups: Sequence[str] = (), only_groups: bool = False, all_extras: bool = False, no_install_project: bool = False, frozen: bool = True, sealed: bool = False, offline: bool = False, explicit: bool = False, timeout: int = 1800, @@ -57,7 +57,7 @@ def build_environment( out.mkdir(parents=True) try: environment.create() - environment.sync(source, extras=extras, groups=groups, all_extras=all_extras, + environment.sync(source, extras=extras, groups=groups, only_groups=only_groups, all_extras=all_extras, no_install_project=no_install_project, frozen=frozen, timeout=timeout) environment.check() if sealed: diff --git a/setup.py b/setup.py index 328d53678a..b195853c5c 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ _BLOCK_MESSAGE = ( "See: https://hermes-agent.nousresearch.com/docs/getting-started/installation\n" "\n" "If you are developing, use an editable install instead:\n" - " uv sync # or: uv pip install -e .\n" + " source ./activate # PowerShell: . .\\activate.ps1\n" "\n" "If you are building with Nix (uv2nix), this error should not fire —\n" "the Hermes Nix derivation sets HERMES_NIX_BUILD=1. If it does, file a bug." diff --git a/tests/pm/test_environment_build.py b/tests/pm/test_environment_build.py index 2f871b747e..be1fb283e1 100644 --- a/tests/pm/test_environment_build.py +++ b/tests/pm/test_environment_build.py @@ -199,6 +199,22 @@ def test_public_dependency_only_build_needs_no_application_source(installable_pr assert not Path(env["HERMES_HOME"]).exists() +def test_group_only_build_excludes_application_dependencies(locked_project, tmp_path): + import pm + + source, _, env = locked_project + metadata = source / "pyproject.toml" + metadata.write_text(metadata.read_text() + '\n[dependency-groups]\nicons=["chosen-dep==1.0"]\n') + pm.lock_project(source, python=Path(sys.executable), cache=tmp_path / "cache", env=env, + offline=True, explicit=True) + python = pm.build_environment(source=source, out=tmp_path / "icons", groups=["icons"], + only_groups=True, python=Path(sys.executable), cache=tmp_path / "cache", + env=env, offline=True, explicit=True) + assert _run([str(python), "-I", "-c", "import chosen_dep, importlib.util; " + "assert importlib.util.find_spec('base_dep') is None; print(chosen_dep.__version__)"], + cwd=tmp_path, env=env) == "1.0" + + def test_child_output_is_live_and_keeps_explicit_index_credentials(tmp_path): import io from pm.environment import PythonEnvironment diff --git a/website/docs/reference/package-management.md b/website/docs/reference/package-management.md index 72aa3fa5a1..031a061eb1 100644 --- a/website/docs/reference/package-management.md +++ b/website/docs/reference/package-management.md @@ -356,6 +356,7 @@ Use the public `pm` module for Python dependency work: | `pm.sync_venv(extras, explicit=True)` | Prepare and select the complete application dependency union, including enabled plugins. | | `pm.sync_venv(repair=True, explicit=True)` | Replay the recorded dependency set in a new application generation. | | `pm.build_environment(source=..., out=..., explicit=True)` | Build and validate a fresh caller-owned output. No plugin discovery or application selection. | +| `pm.build_environment(source=..., out=..., groups=[...], only_groups=True, explicit=True)` | Build only the selected locked dependency groups, without application dependencies. Used by icon builds. | | `pm.lock_project(source, explicit=True)` | Refresh an explicit project's lock without selecting an environment. | | `pm.ensure_environment(name, requirements, explicit=True)` | Prepare and select an isolated dependency generation. Return its Python path. | | `pm.ensure_python_tool(name, requirements, executable, explicit=True)` | Prepare an isolated tool and return its executable path. |