Skip to content
1 change: 1 addition & 0 deletions .github/agents/docs.agent.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
description: 'Maintain Plugboard documentation'
tools: ['execute', 'read', 'edit', 'search', 'web', 'github.vscode-pull-request-github/activePullRequest', 'ms-python.python/getPythonEnvironmentInfo', 'ms-python.python/getPythonExecutableCommand', 'ms-python.python/installPythonPackage', 'ms-python.python/configurePythonEnvironment', 'todo']
model: ['GPT-5 mini (copilot)', 'GPT-4.1 (copilot)']
---

You are an expert technical writer responsible for maintaining the documentation of the Plugboard project. You write for a technical audience includeing developers, data scientists and domain experts who want to build models in Plugboard.
Expand Down
42 changes: 42 additions & 0 deletions .github/agents/examples.agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
name: examples
description: Develops example Plugboard models to demonstrate the capabilities of the framework
argument-hint: A description of the example to generate, along with any specific requirements, ideas about structure, or constraints.
agents: ['researcher', 'docs', 'lint']
---

You are responsible for building high quality tutorials and demo examples for the Plugboard framework. These may be to showcase specific features of the framework, to demonstrate how to build specific types of models, or to provide examples of how Plugboard can be used for different use-cases and business domains.

## Your role:
- If you are building a tutorial:
- Create tutorials in the `examples/tutorials` directory that provide step-by-step guidance on how to build models using the Plugboard framework. These should be detailed and easy to follow, with clear explanations of each step in the process.
- Create markdown documentation alongside code. You can delegate to the `docs` subagent to make these updates.
- Focus on runnable code with expected outputs, so that users can easily follow along and understand the concepts being taught.
- If you are building a demo example:
- Create demo examples in the `examples/demos` directory that demonstrate specific use-cases. These should be well-documented and include explanations of the code and the reasoning behind design decisions.
- Prefer Jupyter notebooks for demo examples, as these allow for a mix of code, documentation and visualizations that can help to illustrate the concepts being demonstrated.
- If the user asks you to research a specific topic related to an example, delegate to the `researcher` subagent to gather relevant information and insights that can inform the development of the example.


## Jupyter Notebooks:
Use the following guidelines when creating demo notebooks:
1. **Structure**
- Demo notebooks should be organized by domain into folders
- Title markdown cell in the same format as the other notebooks, including badges to run on Github/Colab
- Clear markdown sections
- Code cells with explanations
- Visualizations of results
- Summary of findings
2. **Best Practices**
- Keep cells focused and small
- Add docstrings to helper functions
- Show intermediate results
- Include error handling
3. **Output**
- Clear cell output before committing
- Generate plots where helpful
- Provide interpretation of results

## Boundaries:
- **Always** run the lint subagent on any code you write to ensure it adheres to the project's coding standards and is fully type-annotated.
- **Never** edit files outside of `examples/` and `docs/` without explicit instructions to do so, as your focus should be on building examples and maintaining documentation.
1 change: 1 addition & 0 deletions .github/agents/lint.agent.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
description: 'Maintain code quality by running linting tools and resolving issues'
tools: ['execute', 'read', 'edit', 'search', 'ms-python.python/getPythonEnvironmentInfo', 'ms-python.python/getPythonExecutableCommand', 'ms-python.python/installPythonPackage', 'ms-python.python/configurePythonEnvironment']
model: ['GPT-5 mini (copilot)', 'GPT-4.1 (copilot)']
---

You are responsible for maintaining code quality in the Plugboard project by running linting tools and resolving any issues that arise.
Expand Down
20 changes: 20 additions & 0 deletions .github/agents/researcher.agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
name: researcher
description: Researches specific topics on the internet and gathers relevant information.
argument-hint: A clear description of the model or topic to research, along with any specific questions to answer, sources to consult, or types of information to gather.
tools: ['vscode', 'read', 'agent', 'search', 'web', 'todo']
---

You are a subject-matter expert researcher responsible for gathering information on specific topics related to the Plugboard project. Your research will help to inform the development of model components and overall design.

## Your role:
Focus on gathering information about:
- Approaches to modeling the specific process or system being researched, including any relevant theories, frameworks, or best practices
- How the model or simulation can be structured into components, and what the inputs and outputs of those components should be
- What the data flow between components should look like, and any data structures required
- Any specific algorithms or equations that need to be implemented inside the components

## Boundaries:
- **Always** provide clear and concise summaries of the information you gather.
- Use internet search tools to find relevant information, but critically evaluate the credibility and relevance of sources before including them in your summaries.
- If the NotebookLM tool is available, use it to read and summarize relevant documents, papers or articles. Ask the user to upload any documents that are relevant to the research topic.
44 changes: 2 additions & 42 deletions examples/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
# AI Agent Instructions for Plugboard Examples
# AI Agent Instructions for Plugboard Models

This document provides guidelines for AI agents working with Plugboard example code, demonstrations, and tutorials.

## Purpose

These examples demonstrate how to use Plugboard to model and simulate complex processes. Help users build intuitive, well-documented examples that showcase Plugboard's capabilities.

## Example Categories

### Tutorials (`tutorials/`)

Step-by-step learning materials for new users. Focus on:
- Clear explanations of concepts.
- Progressive complexity.
- Runnable code with expected outputs.
- Markdown documentation alongside code. You can delegate to the `docs` agent to make these updates.

### Demos (`demos/`)

Practical applications are organized by domain into folders.
This document provides guidelines for AI agents working with specific models implemented in Plugboard.

## Creating a Plugboard model

Expand Down Expand Up @@ -244,28 +226,6 @@ Later, load and run via CLI
plugboard process run my-model.yaml
```

## Jupyter Notebooks

Use the following guidelines when creating demo notebooks:

1. **Structure**
- Title markdown cell in the same format as the other notebooks, including badges to run on Github/Colab
- Clear markdown sections
- Code cells with explanations
- Visualizations of results
- Summary of findings

2. **Best Practices**
- Keep cells focused and small
- Add docstrings to helper functions
- Show intermediate results
- Include error handling

3. **Output**
- Clear cell output before committing
- Generate plots where helpful
- Provide interpretation of results

## Resources

- **Library Components**: `plugboard.library`
Expand Down
2 changes: 2 additions & 0 deletions plugboard/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import typer

from plugboard import __version__
from plugboard.cli.go import app as go_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
Expand All @@ -14,6 +15,7 @@
help=f"[bold]Plugboard CLI[/bold]\n\nVersion {__version__}",
pretty_exceptions_show_locals=False,
)
app.add_typer(go_app, name="go")
app.add_typer(process_app, name="process")
app.add_typer(server_app, name="server")
app.add_typer(version_app, name="version")
1 change: 1 addition & 0 deletions plugboard/cli/go/AGENTS.md
35 changes: 35 additions & 0 deletions plugboard/cli/go/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Plugboard Go CLI - interactive AI-powered model builder."""

import typer

from plugboard.utils.dependencies import depends_on_optional


app = typer.Typer(rich_markup_mode="rich", pretty_exceptions_show_locals=False)


@depends_on_optional("textual", extra="go")
@depends_on_optional("copilot", extra="go")
def _run_go(model: str) -> None:
"""Launch the Plugboard Go TUI."""
from plugboard.cli.go.app import PlugboardGoApp

tui = PlugboardGoApp(model_name=model)
tui.run()


@app.callback(invoke_without_command=True)
def go(
model: str = typer.Option(
"gpt-5-mini",
"--model",
"-m",
help="LLM model to use (e.g. gpt-5-mini, claude-sonnet-4, gpt-5.4).",
),
) -> None:
"""Launch the interactive Plugboard model builder powered by GitHub Copilot."""
try:
_run_go(model)
except ImportError as exc:
typer.echo(str(exc))
raise typer.Exit(1) from exc
198 changes: 198 additions & 0 deletions plugboard/cli/go/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
"""Copilot SDK agent integration for Plugboard Go."""

from __future__ import annotations

from importlib import resources
from pathlib import Path
import typing as _t

from copilot import CopilotClient, CopilotSession, PermissionHandler, SessionConfig
from copilot.types import UserInputRequest, UserInputResponse

from plugboard.cli.go.tools import (
create_mermaid_diagram_tool,
create_run_model_tool,
)


_FALLBACK_SYSTEM_PROMPT = (
"You are a helpful assistant that helps users design and implement "
"Plugboard models. Plugboard is an event-driven modelling framework "
"in Python."
)


def _load_system_prompt() -> str:
"""Load the system prompt from bundled package data.

The AGENTS.md file is shipped inside the ``plugboard.cli.go`` package
so that it is available even when plugboard is installed from a wheel.
Falls back to ``examples/AGENTS.md`` in the working directory for
local development, or to a short built-in prompt if neither is found.
"""
# 1. Load from package data (works in installed wheels)
try:
ref = resources.files("plugboard.cli.go").joinpath("AGENTS.md")
text = ref.read_text(encoding="utf-8")
if text:
return text
except (FileNotFoundError, TypeError, ModuleNotFoundError):
pass

# 2. Fallback for local dev: check examples/ in the working directory
cwd_candidate = Path.cwd() / "examples" / "AGENTS.md"
if cwd_candidate.exists():
return cwd_candidate.read_text(encoding="utf-8")

return _FALLBACK_SYSTEM_PROMPT


class PlugboardAgent:
"""Manages the Copilot client and session for the Plugboard Go TUI."""

_PREFERRED_MODEL_PREFIXES = (
"gpt-5-mini",
"gpt-4.1",
"gpt",
"o3",
"o1",
"claude",
)

def __init__(
self,
model: str,
on_assistant_delta: _t.Callable[[str], None] | None = None,
on_assistant_message: _t.Callable[[str], None] | None = None,
on_tool_start: _t.Callable[[str], None] | None = None,
on_user_input_request: _t.Callable[
[UserInputRequest, dict[str, str]],
_t.Awaitable[UserInputResponse],
]
| None = None,
on_mermaid_url: _t.Callable[[str], None] | None = None,
on_idle: _t.Callable[[], None] | None = None,
) -> None:
self._model = model
self._on_assistant_delta = on_assistant_delta
self._on_assistant_message = on_assistant_message
self._on_tool_start = on_tool_start
self._on_user_input_request_cb = on_user_input_request
self._on_mermaid_url = on_mermaid_url
self._on_idle = on_idle
self._client: CopilotClient | None = None
self._session: CopilotSession | None = None

@property
def model(self) -> str:
"""Return the current model name."""
return self._model

async def start(self) -> None:
"""Start the Copilot client and create a session."""
self._client = CopilotClient({"log_level": "error"})
await self._client.start()
self._model = await self._resolve_model(self._model)

system_prompt = _load_system_prompt()

tools = [
create_run_model_tool(),
create_mermaid_diagram_tool(on_url_generated=self._on_mermaid_url),
]

session_config = SessionConfig(
model=self._model,
streaming=True,
tools=tools,
system_message={
"content": system_prompt,
},
on_permission_request=PermissionHandler.approve_all,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

The use of PermissionHandler.approve_all automatically grants the LLM permission to execute any registered tool without user intervention. When combined with tools that can execute code or access the file system (like run_plugboard_model), this creates a significant security risk. An attacker could use prompt injection to trick the LLM into executing malicious code on the user's machine. It is highly recommended to implement a permission handler that requires explicit user confirmation for sensitive tool executions.

)

if self._on_user_input_request_cb is not None:
session_config["on_user_input_request"] = self._on_user_input_request_cb

self._session = await self._client.create_session(session_config)
self._session.on(self._handle_event)

async def _resolve_model(self, requested_model: str) -> str:
"""Resolve the requested model to an available preferred model."""
if self._client is None:
return requested_model

try:
models = await self._client.list_models()
except Exception:
return requested_model
Comment on lines +127 to +128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catching a broad Exception can hide unexpected errors. It would be better to catch a more specific exception if the copilot SDK provides one for this case. If not, consider logging the exception at a DEBUG or WARNING level to aid in debugging potential issues with model resolution.


model_ids = [model.id for model in models] if models else []
if not model_ids:
return requested_model
if requested_model in model_ids:
return requested_model

for prefix in self._PREFERRED_MODEL_PREFIXES:
for model_id in model_ids:
if model_id.startswith(prefix):
return model_id

return model_ids[0]

def _handle_event(self, event: _t.Any) -> None:
"""Route session events to callbacks."""
event_type = event.type.value if hasattr(event.type, "value") else str(event.type)

if event_type == "assistant.message_delta":
delta = event.data.delta_content or ""
if delta and self._on_assistant_delta:
self._on_assistant_delta(delta)
elif event_type == "assistant.message":
content = event.data.content or ""
if self._on_assistant_message:
self._on_assistant_message(content)
elif event_type == "tool.execution_start":
tool_name = event.data.tool_name if hasattr(event.data, "tool_name") else "unknown"
if self._on_tool_start:
self._on_tool_start(tool_name)
elif event_type == "session.idle":
if self._on_idle:
self._on_idle()

async def send(self, prompt: str) -> None:
"""Send a user prompt to the agent."""
if self._session is None:
raise RuntimeError("Agent not started. Call start() first.")
await self._session.send({"prompt": prompt})

async def list_models(self) -> list[str]:
"""List available models."""
if self._client is None:
raise RuntimeError("Agent not started. Call start() first.")
try:
models = await self._client.list_models()
return [m.id for m in models] if models else []
except Exception:
return ["gpt-4o", "gpt-5", "claude-sonnet-4", "claude-sonnet-4-thinking", "o3"]
Comment on lines +176 to +177
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the _resolve_model method, catching a broad Exception here is not ideal as it can hide the root cause of failures. It would be better to catch a more specific exception. Also, consider logging the exception to provide more context when the fallback list of models is returned. This will make it easier to diagnose connection issues with the Copilot service.


async def change_model(self, model: str) -> None:
"""Change the model by destroying and recreating the session."""
self._model = model
await self.stop()
await self.start()

async def stop(self) -> None:
"""Clean up the Copilot client and session."""
if self._session is not None:
try:
await self._session.destroy()
except Exception: # noqa: S110
pass # Best-effort cleanup
self._session = None
if self._client is not None:
try:
await self._client.stop()
except Exception: # noqa: S110
pass # Best-effort cleanup
self._client = None
Loading
Loading