Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ User applications to help with tasks using diffpy packages. Currently it
contains

- `runmacro`: A runner for DiffPy macro files.
- `agentify`: A deployer for diffpy.cmi agentic skills.

For more information about the diffpy.apps library, please consult our `online documentation <https://diffpy.github.io/diffpy.apps>`_.

Expand Down
31 changes: 31 additions & 0 deletions docs/source/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ diffpy packages. This page contains the instructions for all applications
available, including:

- :ref:`runmacro`
- :ref:`agentify`

.. _runmacro:

Expand Down Expand Up @@ -149,3 +150,33 @@ starting point for the refinement.
constraint. e.g. Here, lattice parameter ``a=b=c``,
and ``Usio_0=Uiso_i, i=1,2,3``, ``a`` and ``Uiso_0`` are used as the
reference variables.

.. _agentify:
Use ``agentify`` to deploy agent skills ``diffpy.cmi``
------------------------------------------------------

The ``agentify`` application allows users to deploy agentic skills in the
local environment. To use this application, run:

.. code-block:: bash

diffpy.app agentify

``claude`` and ``codex`` agent skills are supported, and ``claude`` is used
by default. To specify the agent skill, use the ``--agent`` option:

.. code-block:: bash

diffpy.app agentify --agent codex

To deploy the agentic skill to the system directory, use the ``--system`` flag:

.. code-block:: bash

diffpy.app agentify --system

To update the existing ``diffpy.cmi`` agentic skill, use the ``--update`` flag:

.. code-block:: bash

diffpy.app agentify --update
23 changes: 23 additions & 0 deletions news/agentify.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**Added:**

* Add "agentify" app to deploy ``diffpy.cmi`` agent skills.

**Changed:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
35 changes: 35 additions & 0 deletions src/diffpy/apps/app_agentify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import shutil
import subprocess
import tempfile
from pathlib import Path

REPO_URL = "https://github.com/diffpy/cmi-agent-skills"
DIR_NAME = "cmi-skill"


def agentify(args):
agent = args.agent
system_flag = args.system
if agent == "claude":
skills_dir = ".claude/skills"
elif agent == "codex":
skills_dir = ".codex/skills"
if system_flag:
destination = Path().home() / skills_dir / DIR_NAME
else:
destination = Path().cwd() / skills_dir / DIR_NAME
if destination.exists() and not args.update:
raise FileExistsError(
f"Agentic skill {DIR_NAME} already exists at {destination}. "
"To overwrite, pass '--update' flag to update the skill"
)
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
subprocess.run(
["git", "clone", REPO_URL, str(tmp_path)],
check=True,
)
if destination.exists():
shutil.rmtree(destination)
shutil.copytree(tmp_path / DIR_NAME, destination, dirs_exist_ok=True)
print(f"Agentic skill {DIR_NAME} has been deployed to {destination}")
24 changes: 24 additions & 0 deletions src/diffpy/apps/apps.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import argparse

from diffpy.apps.app_agentify import agentify
from diffpy.apps.app_runmacro import runmacro
from diffpy.apps.version import __version__ # noqa

Expand Down Expand Up @@ -36,6 +37,7 @@ def main():
title="Available applications",
dest="application",
)
# runmacro application
runmacro_parser = apps_parsers.add_parser(
"runmacro",
help="Run a macro `<.dp-in>` file",
Expand All @@ -46,6 +48,28 @@ def main():
help="Path to the `<.dp-in>` macro file to be run",
)
runmacro_parser.set_defaults(func=runmacro)
# agentify application
agentify_parser = apps_parsers.add_parser(
"agentify",
help="Deploy diffpy.cmi agentic skills in the local environment.",
)
agentify_parser.add_argument(
"--agent",
help="The agent to use for the agentic skill.",
default="claude",
choices=["claude", "codex"],
)
agentify_parser.add_argument(
"--update",
action="store_true",
help="When set, update the existing agentic skill.",
)
agentify_parser.add_argument(
"--system",
action="store_true",
help="When set, deploy the agentic skill to the system directory.",
)
agentify_parser.set_defaults(func=agentify)
args = parser.parse_args()
if args.application is None:
parser.print_help()
Expand Down
110 changes: 110 additions & 0 deletions tests/test_agentify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import re
import tempfile
from pathlib import Path
from types import SimpleNamespace
from unittest import mock

import pytest

from diffpy.apps.app_agentify import agentify


@pytest.mark.parametrize(
"args, expected_scope, expected_skill_dir",
[
# C1: diffpy.apps agentify
# Deploys workspace claude skill.
# Expect skill folder is created in the current working directory.
(
SimpleNamespace(
agent="claude",
system=False,
update=False,
),
"cwd",
".claude/skills/cmi-skill",
),
# C2: diffpy.apps agentify --system
# Deploys system claude skill.
# Expect skill folder is created in the user's home directory.
(
SimpleNamespace(
agent="claude",
system=True,
update=False,
),
"home",
".claude/skills/cmi-skill",
),
# C3: diffpy.apps agentify --agent codex
# Deploys workspace codex skill.
# Expect skill folder is created in the current working directory.
(
SimpleNamespace(
agent="codex",
system=False,
update=False,
),
"cwd",
".codex/skills/cmi-skill",
),
# C4: diffpy.apps agentify --agent codex --system
# Deploys system codex skill.
# Expect skill folder is created in the user's home directory.
(
SimpleNamespace(
agent="codex",
system=True,
update=False,
),
"home",
".codex/skills/cmi-skill",
),
],
)
def test_agentify(args, expected_scope, expected_skill_dir):
with tempfile.TemporaryDirectory() as tmp:
with (
mock.patch.object(Path, "home", return_value=Path(tmp) / "home"),
mock.patch.object(Path, "cwd", return_value=Path(tmp) / "cwd"),
):
agentify(args)
expected_path = Path(tmp) / expected_scope / expected_skill_dir
assert expected_path.exists()


def test_agentify_update():
with tempfile.TemporaryDirectory() as tmp:
with (
mock.patch.object(Path, "home", return_value=Path(tmp) / "home"),
mock.patch.object(Path, "cwd", return_value=Path(tmp) / "cwd"),
):
# C1: Deploy again without --update flag when skill already exists.
# Expect FileExistsError to be raised, and the error message
# matches.
args = SimpleNamespace(
agent="claude",
system=False,
update=False,
)
agentify(args)
skill_path = Path(tmp) / "cwd" / ".claude" / "skills" / "cmi-skill"
assert skill_path.exists()
args.update = True
agentify(args)
pytest.raises(
FileExistsError,
match=re.escape(
f"Agentic skill cmi-skill already exists at {skill_path}. "
"To overwrite, pass '--update' flag to update the skill"
),
)
# C2: Deploy again with --update flag when skill already exists
# with a dummy file in the skill directory.
# Expect no error to be raised, and the skill is updated.
dummy_file = skill_path / "dummy.txt"
dummy_file.touch()
assert dummy_file.exists()
args.update = True
agentify(args)
assert not dummy_file.exists()
Loading