|
| 1 | +--- |
| 2 | +title: "Work with Canvas" |
| 3 | +description: "Handle artifact editing requests from users" |
| 4 | +--- |
| 5 | + |
| 6 | +The Canvas extension enables users to request edits to specific parts of artifacts (like code, documents, or other structured content) that your agent has generated. Instead of users describing what they want to change in text, they can select a portion of an artifact in the UI and request an edit, giving your agent precise information about what to modify. |
| 7 | + |
| 8 | +## How Canvas Works |
| 9 | + |
| 10 | +When a user selects a portion of an artifact and requests an edit: |
| 11 | + |
| 12 | +1. The selection's **start and end indices** (character positions) are captured |
| 13 | +2. The user provides a **description** of what they want to change |
| 14 | +3. The **artifact ID** identifies which artifact to edit |
| 15 | +4. Your agent receives this structured information to process the edit request |
| 16 | + |
| 17 | +This allows your agent to: |
| 18 | + |
| 19 | +- Know exactly which part of the artifact the user wants to modify |
| 20 | +- Access the original artifact content from history |
| 21 | +- Make targeted edits based on the user's description |
| 22 | +- Generate a new artifact with the changes (using the same artifact_id to replace the previous version in the UI) |
| 23 | + |
| 24 | +## Quickstart |
| 25 | + |
| 26 | +<Steps> |
| 27 | +<Step title="Import the canvas extension"> |
| 28 | +Import `CanvasExtensionServer` and `CanvasExtensionSpec` from `agentstack_sdk.a2a.extensions.ui.canvas`. |
| 29 | +</Step> |
| 30 | + |
| 31 | +<Step title='Inject the extension'> |
| 32 | + Add a canvas parameter to your agent function using the `Annotated` type hint |
| 33 | + with `CanvasExtensionSpec()`. |
| 34 | +</Step> |
| 35 | + |
| 36 | +<Step title='Parse edit requests'> |
| 37 | + Call `await canvas.parse_canvas_edit_request(message=message)` to check if the |
| 38 | + incoming message contains a canvas edit request. This returns `None` if no |
| 39 | + edit request is present, or a `CanvasEditRequest` object with: - |
| 40 | + `start_index`: The starting character position of the selected text - |
| 41 | + `end_index`: The ending character position of the selected text - |
| 42 | + `description`: The user's description of what they want to change - |
| 43 | + `artifact`: The full original artifact object from history |
| 44 | +</Step> |
| 45 | + |
| 46 | +<Step title='Access the original content'> |
| 47 | + Extract the text from `artifact.parts[0].root.text` (for text artifacts) and |
| 48 | + use the start/end indices to get the selected portion: `selected_text = |
| 49 | + content[start_index:end_index]`. |
| 50 | +</Step> |
| 51 | + |
| 52 | +<Step title="Return a new artifact"> |
| 53 | +Create a new artifact with your changes. |
| 54 | +</Step> |
| 55 | +</Steps> |
| 56 | + |
| 57 | +## Example: Canvas with LLM |
| 58 | + |
| 59 | +Here's how to use canvas with an LLM, adapting your system prompt based on whether you're generating new content or editing existing content: |
| 60 | + |
| 61 | +````python |
| 62 | +# Copyright 2025 © BeeAI a Series of LF Projects, LLC |
| 63 | +# SPDX-License-Identifier: Apache-2.0 |
| 64 | + |
| 65 | +import re |
| 66 | +from typing import Annotated |
| 67 | + |
| 68 | +from a2a.types import Message, TextPart |
| 69 | + |
| 70 | +from agentstack_sdk.a2a.extensions import LLMServiceExtensionServer, LLMServiceExtensionSpec |
| 71 | +from agentstack_sdk.a2a.extensions.ui.canvas import ( |
| 72 | + CanvasExtensionServer, |
| 73 | + CanvasExtensionSpec, |
| 74 | +) |
| 75 | +from agentstack_sdk.a2a.types import AgentArtifact |
| 76 | +from agentstack_sdk.server import Server |
| 77 | +from agentstack_sdk.server.context import RunContext |
| 78 | + |
| 79 | +server = Server() |
| 80 | + |
| 81 | +BASE_PROMPT = """\ |
| 82 | +You are a helpful coding assistant. |
| 83 | +Generate code enclosed in triple-backtick blocks tagged ```python. |
| 84 | +The first line should be a comment with the code's purpose. |
| 85 | +""" |
| 86 | + |
| 87 | +EDIT_PROMPT = ( |
| 88 | + BASE_PROMPT |
| 89 | + + """ |
| 90 | +You are editing existing code. The user selected this portion: |
| 91 | +```python |
| 92 | +{selected_code} |
| 93 | +``` |
| 94 | +
|
| 95 | +They want: {description} |
| 96 | +
|
| 97 | +Respond with the FULL updated code. Only change the selected portion. |
| 98 | +""" |
| 99 | +) |
| 100 | + |
| 101 | + |
| 102 | +def get_system_prompt(canvas_edit): |
| 103 | + if not canvas_edit: |
| 104 | + return BASE_PROMPT |
| 105 | + |
| 106 | + original_code = ( |
| 107 | + canvas_edit.artifact.parts[0].root.text if isinstance(canvas_edit.artifact.parts[0].root, TextPart) else "" |
| 108 | + ) |
| 109 | + selected = original_code[canvas_edit.start_index : canvas_edit.end_index] |
| 110 | + |
| 111 | + return EDIT_PROMPT.format(selected_code=selected, description=canvas_edit.description) |
| 112 | + |
| 113 | + |
| 114 | +@server.agent() |
| 115 | +async def code_agent( |
| 116 | + message: Message, |
| 117 | + context: RunContext, |
| 118 | + llm: Annotated[LLMServiceExtensionServer, LLMServiceExtensionSpec.single_demand()], |
| 119 | + canvas: Annotated[CanvasExtensionServer, CanvasExtensionSpec()], |
| 120 | +): |
| 121 | + canvas_edit = await canvas.parse_canvas_edit_request(message=message) |
| 122 | + |
| 123 | + # Adapt system prompt based on whether this is an edit or new generation |
| 124 | + system_prompt = get_system_prompt(canvas_edit) |
| 125 | + |
| 126 | + # Call your LLM with the adapted prompt |
| 127 | + # (implementation depends on your LLM framework) |
| 128 | + response = await call_llm(llm, system_prompt, message) |
| 129 | + |
| 130 | + # Extract code from response |
| 131 | + match = re.search(r"```python\n(.*?)\n```", response, re.DOTALL) |
| 132 | + if match: |
| 133 | + code = match.group(1).strip() |
| 134 | + first_line = code.split("\n", 1)[0] |
| 135 | + name = first_line.lstrip("# ").strip() if first_line.startswith("#") else "Python Script" |
| 136 | + |
| 137 | + artifact = AgentArtifact( |
| 138 | + name=name, |
| 139 | + parts=[TextPart(text=code)], |
| 140 | + ) |
| 141 | + yield artifact |
| 142 | + |
| 143 | + # Note: Always create a new artifact. When canvas_edit exists, pass its artifact_id |
| 144 | + # to replace the previous version in the UI. |
| 145 | + |
| 146 | + |
| 147 | +if __name__ == "__main__": |
| 148 | + server.run() |
| 149 | +```` |
| 150 | + |
| 151 | +## How to work with Canvas |
| 152 | + |
| 153 | +**Artifacts in history**: The extension automatically retrieves the original artifact from history using the `artifact_id`. If not found, a `ValueError` is raised. |
| 154 | + |
| 155 | +**Text parts filtering**: The extension filters out fallback text messages (sent for agents that don't support canvas) so you only work with structured edit request data. |
| 156 | + |
| 157 | +## Best Practices |
| 158 | + |
| 159 | +**Adapt your system prompt**: Provide different instructions to your LLM depending on whether you're generating new content or editing existing content. |
| 160 | + |
| 161 | +**Validate indices**: Ensure start and end indices are within bounds before slicing the artifact text. |
| 162 | + |
| 163 | +``` |
| 164 | +
|
| 165 | +``` |
0 commit comments