-
Notifications
You must be signed in to change notification settings - Fork 0
feat: LLM-powered model building tool plugboard go
#224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
23558ad
6ace083
99a4dff
8bb005f
a43ee62
6e561c2
c14d586
e6c6f34
481dba1
14021c2
4b2724c
d3a137b
fd7b9be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| 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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../../../examples/AGENTS.md | ||
| 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 |
| 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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of |
||
| ) | ||
|
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the |
||
|
|
||
| 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 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The content of this file is a relative path, but the code in
plugboard/cli/go/agent.pyreads this file's content directly to use as a system prompt. This will result in the prompt being the literal string../../../examples/AGENTS.md, which is not the intended behavior and will cause thegocommand to function incorrectly when the package is installed. To fix this, you should replace the path with the actual content ofexamples/AGENTS.md.