diff --git a/docs/usage/ai.md b/docs/usage/ai.md new file mode 100644 index 00000000..421e78dd --- /dev/null +++ b/docs/usage/ai.md @@ -0,0 +1,42 @@ +# AI-Assisted Development + +Plugboard ships with tooling to help AI coding agents understand how to build models using the framework. The `plugboard ai` command group provides utilities for setting up AI-assisted development workflows. + +## Initialising a project + +The `plugboard ai init` command creates an `AGENTS.md` file in your project directory. This file gives AI coding agents the context they need to help you build Plugboard models — covering how to create components, assemble processes, use events, and follow best practices. + +`AGENTS.md` is a convention used by AI coding tools (such as [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview), [Codex](https://openai.com/index/codex/), and [Gemini CLI](https://github.com/google-gemini/gemini-cli)) to discover project-specific instructions automatically. + +### Usage + +To create an `AGENTS.md` file in the current working directory: + +```bash +plugboard ai init +``` + +To create the file in a specific directory: + +```bash +plugboard ai init /path/to/project +``` + +!!! note + The command will not overwrite an existing `AGENTS.md` file. If one already exists in the target directory, the command exits with an error. + +### What's in the file? + +The generated `AGENTS.md` covers: + +- **Planning a model** — how to break a problem down into components, inputs, outputs, and data flows. +- **Implementing components** — using built-in library components and creating custom ones by subclassing [`Component`][plugboard.component.Component]. +- **Assembling a process** — connecting components together and running a [`LocalProcess`][plugboard.process.LocalProcess]. +- **Event-driven models** — defining custom [`Event`][plugboard.events.Event] types, emitting events, and writing event handlers. +- **Exporting models** — saving process definitions to YAML and running them via the CLI. + +The file is intended to be committed to version control alongside your project code so that any AI agent working in the repository has immediate access to Plugboard conventions. + +### Customising + +After generating the file you can edit it freely to add project-specific instructions — for example, domain context, coding standards, or pointers to your own components and data sources. diff --git a/examples/AGENTS.md b/examples/AGENTS.md index 8917cfdd..19813889 100644 --- a/examples/AGENTS.md +++ b/examples/AGENTS.md @@ -38,7 +38,7 @@ Ask questions if anything is not clear about the business logic or you require a Always check whether the functionality you need is already available in the library components in `plugboard.library`. For example, try to use: - `FileReader` and `FileWriter` for reading/writing data from CSV or parquet files. -- `SQLReader` and `SQLReader` for reading/writing data from SQL databases. +- `SQLReader` and `SQLWriter` for reading/writing data from SQL databases. - `LLMChat` for interacting with standard LLMs, e.g. OpenAI, Gemini, etc. **Using Built-in Components** @@ -51,7 +51,7 @@ data_loader = FileReader(name="input_data", path="input.csv", field_names=["x", **Creating Custom Components** -New components should inherit from `plugboard.componen.Component`. Add logging messages where it would be helpful by using the bound logger `self._logger`. +New components should inherit from `plugboard.component.Component`. Add logging messages where it would be helpful by using the bound logger `self._logger`. ```python import typing as _t diff --git a/mkdocs.yaml b/mkdocs.yaml index 199c291e..22be5a94 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -129,6 +129,7 @@ nav: - Event-driven models: examples/tutorials/event-driven-models.md - Tuning a process: examples/tutorials/tuning-a-process.md - Configuration: usage/configuration.md + - AI-Assisted Development: usage/ai.md - Topics: usage/topics.md - Demos: - Fundamentals: diff --git a/plugboard/cli/__init__.py b/plugboard/cli/__init__.py index 33a46c31..e50dd099 100644 --- a/plugboard/cli/__init__.py +++ b/plugboard/cli/__init__.py @@ -3,6 +3,7 @@ import typer from plugboard import __version__ +from plugboard.cli.ai import app as ai_app from plugboard.cli.process import app as process_app from plugboard.cli.server import app as server_app from plugboard.cli.version import app as version_app @@ -14,6 +15,7 @@ help=f"[bold]Plugboard CLI[/bold]\n\nVersion {__version__}", pretty_exceptions_show_locals=False, ) +app.add_typer(ai_app, name="ai") app.add_typer(process_app, name="process") app.add_typer(server_app, name="server") app.add_typer(version_app, name="version") diff --git a/plugboard/cli/ai/AGENTS.md b/plugboard/cli/ai/AGENTS.md new file mode 120000 index 00000000..17086c0c --- /dev/null +++ b/plugboard/cli/ai/AGENTS.md @@ -0,0 +1 @@ +../../../examples/AGENTS.md \ No newline at end of file diff --git a/plugboard/cli/ai/__init__.py b/plugboard/cli/ai/__init__.py new file mode 100644 index 00000000..b52c15c4 --- /dev/null +++ b/plugboard/cli/ai/__init__.py @@ -0,0 +1,41 @@ +"""Plugboard AI CLI.""" + +from pathlib import Path +import shutil + +from rich import print +from rich.console import Console +import typer + + +app = typer.Typer( + rich_markup_mode="rich", no_args_is_help=True, pretty_exceptions_show_locals=False +) +stderr = Console(stderr=True) + +_AGENTS_MD = Path(__file__).parent / "AGENTS.md" + + +@app.command() +def init( + directory: Path = typer.Argument( + default=None, + help="Target directory for the AGENTS.md file. Defaults to the current working directory.", + exists=True, + file_okay=False, + dir_okay=True, + resolve_path=True, + ), +) -> None: + """Initialise a project with an AGENTS.md file for AI-assisted development.""" + if directory is None: + directory = Path.cwd() + + target = directory / "AGENTS.md" + + if target.exists(): + stderr.print("[red]AGENTS.md already exists[/red] in the target directory.") + raise typer.Exit(1) + + shutil.copy2(_AGENTS_MD, target) + print(f"[green]Created[/green] {target}") diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index cb3f429c..0aa9248a 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -87,6 +87,42 @@ def test_cli_process_diagram() -> None: assert "flowchart" in result.stdout +def test_cli_ai_init(tmp_path: Path) -> None: + """Tests the ai init command creates AGENTS.md.""" + result = runner.invoke(app, ["ai", "init", str(tmp_path)]) + assert result.exit_code == 0 + assert "Created" in result.stdout + # File must exist with expected content + agents_md = tmp_path / "AGENTS.md" + assert agents_md.exists() + content = agents_md.read_text() + assert "Plugboard" in content + + +def test_cli_ai_init_already_exists(tmp_path: Path) -> None: + """Tests the ai init command fails when AGENTS.md already exists.""" + (tmp_path / "AGENTS.md").write_text("existing content") + result = runner.invoke(app, ["ai", "init", str(tmp_path)]) + assert result.exit_code == 1 + # Error is printed to stderr which typer captures in output + assert "already exists" in result.output + + +def test_cli_ai_init_default_directory() -> None: + """Tests the ai init command uses current directory by default.""" + with tempfile.TemporaryDirectory() as tmpdir: + original_cwd = Path.cwd() + try: + import os + + os.chdir(tmpdir) + result = runner.invoke(app, ["ai", "init"]) + assert result.exit_code == 0 + assert (Path(tmpdir) / "AGENTS.md").exists() + finally: + os.chdir(original_cwd) + + def test_cli_server_discover(test_project_dir: Path) -> None: """Tests the server discover command.""" with respx.mock: