e2244a3cae
- src/ 页面与组件、public/ 静态资源 - 工程配置:vite/tsconfig/package 依赖
2097 lines
64 KiB
Markdown
2097 lines
64 KiB
Markdown
# Plugin System
|
||
|
||
QwenPaw provides a plugin system that allows users to extend QwenPaw's functionality.
|
||
|
||
## Overview
|
||
|
||
The plugin system supports the following extension capabilities:
|
||
|
||
- **Provider Plugins**: Add new LLM providers and models
|
||
- **Middleware Plugins**: Register AgentScope `MiddlewareBase` factories to wrap `on_acting` / `on_reasoning` hooks in the agent reasoning loop
|
||
- **Hook Plugins**: Execute custom code during application startup/shutdown (app lifespan level, runs once)
|
||
- **Command Plugins**: Register custom `/command` magic commands
|
||
- **HTTP API Plugins**: Expose custom REST endpoints under `/api` via a FastAPI `APIRouter`
|
||
- **Frontend Extension Plugins**: Browser-side JS plugins that share the host's React / Ant Design runtime and declaratively extend the UI via `window.QwenPaw.*` API — register sidebar menus, page routes, UI slots, chat customizations, and more without modifying host code
|
||
- **Channel Plugins**: Register custom messaging channels (e.g. Slack, LINE)
|
||
|
||
## Plugin Management
|
||
|
||
### Install Plugin
|
||
|
||
Install from local directory:
|
||
|
||
```bash
|
||
qwenpaw plugin install /path/to/plugin
|
||
```
|
||
|
||
Install from URL (supports ZIP files):
|
||
|
||
```bash
|
||
qwenpaw plugin install https://example.com/plugin.zip
|
||
```
|
||
|
||
Force reinstall:
|
||
|
||
```bash
|
||
qwenpaw plugin install /path/to/plugin --force
|
||
```
|
||
|
||
**Note**: Plugin operations can only be performed when QwenPaw is offline.
|
||
|
||
### List Installed Plugins
|
||
|
||
```bash
|
||
qwenpaw plugin list
|
||
```
|
||
|
||
Example output:
|
||
|
||
```
|
||
Installed Plugins:
|
||
==================
|
||
|
||
my-provider (v1.0.0)
|
||
Custom LLM provider integration
|
||
Author: Developer Name
|
||
Path: /Users/user/.qwenpaw/plugins/my-provider
|
||
```
|
||
|
||
### View Plugin Details
|
||
|
||
```bash
|
||
qwenpaw plugin info <plugin-id>
|
||
```
|
||
|
||
### Uninstall Plugin
|
||
|
||
```bash
|
||
qwenpaw plugin uninstall <plugin-id>
|
||
```
|
||
|
||
## Plugin Development
|
||
|
||
### Backend Plugins
|
||
|
||
#### Basic Structure
|
||
|
||
Each plugin requires at least two files:
|
||
|
||
```
|
||
my-plugin/
|
||
├── plugin.json # Plugin manifest (required)
|
||
├── plugin.py # Entry point (required)
|
||
└── README.md # Documentation (recommended)
|
||
```
|
||
|
||
#### plugin.json
|
||
|
||
```json
|
||
{
|
||
"id": "my-plugin",
|
||
"name": "My Plugin",
|
||
"version": "1.0.0",
|
||
"type": "general",
|
||
"description": "Plugin description",
|
||
"author": "Your Name",
|
||
"entry": {
|
||
"backend": "plugin.py"
|
||
},
|
||
"dependencies": [],
|
||
"qwenpaw_version": {
|
||
"min": "1.0.0",
|
||
"max": "2.1.0"
|
||
},
|
||
"meta": {}
|
||
}
|
||
```
|
||
|
||
#### Manifest Field Reference
|
||
|
||
| Field | Type | Required | Description |
|
||
| ----------------- | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `id` | `string` | yes | Unique plugin identifier. Used as the install directory name; must not contain path separators. |
|
||
| `version` | `string` | yes | Semantic version of the plugin (e.g. `1.0.0`). |
|
||
| `name` | `string` \| object | no | Display name. Defaults to `id`. May also be `{"zh-CN": "...", "en-US": "..."}`; the first non-empty localised value is used (English preferred). |
|
||
| `type` | `string` | no | One of `tool`, `provider`, `hook`, `command`, `frontend`, `general`. When omitted, the type is inferred from `meta` / `entry` (legacy plugins). Prefer setting explicitly. |
|
||
| `description` | `string` \| object | no | Short description shown in the plugin list. Localised form is accepted (see `name`). |
|
||
| `author` | `string` | no | Author or organisation name. |
|
||
| `entry.backend` | `string` | no\* | Path (relative to plugin dir) of the Python entry file that exports `plugin`. |
|
||
| `entry.frontend` | `string` | no\* | Path of the built frontend bundle (e.g. `dist/index.js`). |
|
||
| `dependencies` | `string[]` | no | Python package requirements installed via pip/uv at install time. |
|
||
| `qwenpaw_version` | `object` | no | QwenPaw version constraint (recommended). Contains `min` (inclusive) and `max` (exclusive, optional) sub-fields. Semantics: `>=min, <max`. When `max` is omitted, defaults to `{major}.{minor+1}.0`. |
|
||
| `min_version` | `string` | no | **Legacy.** Minimum QwenPaw version required. Ignored when `qwenpaw_version` is present. Retained only for backward compatibility with third-party plugins. |
|
||
| `max_version` | `string` | no | **Legacy.** First incompatible QwenPaw version (exclusive). Used with `min_version`; when omitted, derived from `min_version`. |
|
||
| `meta` | `object` | no | Free-form plugin metadata. Used by the UI and by `type` inference (e.g. `meta.tools[]`, `meta.hook_type`, `meta.provider_id`). |
|
||
| `entry_point` | `string` | no | **Legacy.** Equivalent to `entry.backend`. Still accepted for backwards compatibility with older plugins; new plugins should use `entry.backend`. |
|
||
|
||
\* At least one of `entry.backend` / `entry.frontend` (or legacy `entry_point`) must be provided.
|
||
|
||
#### `type` values
|
||
|
||
| Value | When to use |
|
||
| ---------- | ---------------------------------------------------------------------- |
|
||
| `tool` | Registers one or more agent tools (functions the LLM can call). |
|
||
| `provider` | Registers a custom LLM provider / model endpoint. |
|
||
| `hook` | Runs code during application startup or shutdown (app lifespan level). |
|
||
| `command` | Registers one or more `/slash` control commands. |
|
||
| `channel` | Registers a custom messaging channel. |
|
||
| `frontend` | Ships a frontend JS bundle loaded dynamically by the UI. |
|
||
| `general` | Fallback for plugins that combine multiple capabilities or don't fit. |
|
||
|
||
#### plugin.py
|
||
|
||
```python
|
||
# -*- coding: utf-8 -*-
|
||
"""My Plugin Entry Point."""
|
||
|
||
from qwenpaw.plugins.api import PluginApi
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class MyPlugin:
|
||
"""My Plugin."""
|
||
|
||
def register(self, api: PluginApi):
|
||
"""Register plugin capabilities.
|
||
|
||
Args:
|
||
api: PluginApi instance
|
||
"""
|
||
logger.info("Registering my plugin...")
|
||
|
||
# Register your capabilities
|
||
# api.register_provider(...)
|
||
# api.register_startup_hook(...)
|
||
# api.register_shutdown_hook(...)
|
||
|
||
logger.info("✓ My plugin registered")
|
||
|
||
|
||
# Export plugin instance
|
||
plugin = MyPlugin()
|
||
```
|
||
|
||
### Frontend Plugins
|
||
|
||
Frontend plugins are JavaScript extensions that run in the browser. Unlike backend plugins that register capabilities via the Python `PluginApi`, frontend plugins declaratively extend the Console UI through the global `window.QwenPaw.*` API.
|
||
|
||
**Loading lifecycle:**
|
||
|
||
1. Console starts up and mounts the Host SDK (React, antd, and other shared dependencies) and registration APIs (menu, route, slot, chat, and other namespaces) on `window.QwenPaw`
|
||
2. Console fetches the enabled frontend plugin list from `/frontend_plugin`
|
||
3. Downloads each plugin's JS bundle and executes it via Blob URL dynamic import
|
||
4. Plugin code runs and calls `window.QwenPaw.*` to register menus, routes, chat customizations, and other UI extensions
|
||
5. Registrations take effect immediately — menus appear in the sidebar, routes become navigable, chat areas show customized content
|
||
|
||
Plugins don't need to declare which extension points they use; the system automatically tracks all registrations via `pluginId`. When a plugin is uninstalled or disabled, all registrations are cleaned up via `dispose()` or `chat.disposeAll(pluginId)`.
|
||
|
||
**Design characteristics:**
|
||
|
||
| Feature | Description |
|
||
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| **Shared runtime** | React, ReactDOM, and Ant Design are provided by the host — plugins don't bundle them, avoiding version conflicts and bloat |
|
||
| **Declarative registration** | Three core verbs: `set` (set / merge properties), `render` (replace rendering), `add` (append items) |
|
||
| **pluginId isolation** | Every registration method takes `pluginId` as the first argument — the system uses it to track origins, detect conflicts, and support per-plugin cleanup |
|
||
| **Revocable** | Every registration returns a `{ dispose() }` object — call it to undo the registration, enabling hot-reload and clean uninstall |
|
||
| **Internationalization** | Text fields support the `Localized<T>` type — pass a `(locale) => string` function to return different values per language |
|
||
|
||
**Extension points at a glance:**
|
||
|
||
| Namespace | Capability | Typical use |
|
||
| --------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------- |
|
||
| `host` | Shared dependencies, React Hooks, authenticated fetch | Access React / antd, read theme and locale, call backend APIs |
|
||
| `menu` | Sidebar menu items | Add navigation entries |
|
||
| `route` | Page routes | Register new pages, wrap existing pages |
|
||
| `slot` | General UI slots | Inject content into Header / Sidebar and other preset positions |
|
||
| `chat.welcome` | Welcome screen | Customize greeting, suggested prompts |
|
||
| `chat.theme` | Chat theme color | Change the primary color |
|
||
| `chat.leftHeader` / `rightHeader` | Chat header | Set brand logo, add action buttons |
|
||
| `chat.sender` | Input box | Custom placeholder, input suggestions |
|
||
| `chat.actions` / `requestActions` | Message action buttons | Add custom actions below messages |
|
||
| `chat.requestPayload` | Outgoing chat request payload | Add custom fields before the request is sent to the backend |
|
||
| `chat.request` / `response` | Message bubbles | Prepend/append content or fully replace rendering |
|
||
| `chat.toolRender` | Tool-call rendering | Custom tool result display (e.g. weather card) |
|
||
| `chat.card` | Custom cards | Register new card types |
|
||
| `audit` | Audit & debugging | View all extension registration records |
|
||
|
||
#### Basic Structure
|
||
|
||
```
|
||
my-plugin/
|
||
├── plugin.json # Plugin manifest (required)
|
||
├── src/
|
||
│ └── index.tsx # Entry point, calls window.QwenPaw.* APIs
|
||
├── package.json # Dependencies
|
||
├── tsconfig.json # TypeScript config
|
||
└── vite.config.ts # Build config
|
||
```
|
||
|
||
#### plugin.json
|
||
|
||
```json
|
||
{
|
||
"id": "my-plugin",
|
||
"name": "My Plugin",
|
||
"version": "1.0.0",
|
||
"type": "frontend",
|
||
"author": "Your Name",
|
||
"entry": { "frontend": "dist/index.js" }
|
||
}
|
||
```
|
||
|
||
#### src/index.tsx
|
||
|
||
The plugin entry file executes on load and registers extensions via `window.QwenPaw.*` API:
|
||
|
||
```tsx
|
||
const { React, antd } = window.QwenPaw.host;
|
||
const pluginId = "my-plugin";
|
||
|
||
// Call window.QwenPaw.* APIs to register menus, routes, chat customizations, etc.
|
||
// See "Frontend Extension API" below for details
|
||
```
|
||
|
||
#### Build Toolchain
|
||
|
||
**package.json**:
|
||
|
||
```json
|
||
{
|
||
"name": "my-plugin",
|
||
"version": "1.0.0",
|
||
"scripts": { "build": "vite build" },
|
||
"devDependencies": {
|
||
"vite": "^5.0.0",
|
||
"typescript": "^5.0.0",
|
||
"@vitejs/plugin-react": "^4.0.0"
|
||
}
|
||
}
|
||
```
|
||
|
||
**tsconfig.json**:
|
||
|
||
```json
|
||
{
|
||
"compilerOptions": {
|
||
"target": "ES2020",
|
||
"module": "ESNext",
|
||
"moduleResolution": "bundler",
|
||
"jsx": "react",
|
||
"strict": false,
|
||
"skipLibCheck": true
|
||
}
|
||
}
|
||
```
|
||
|
||
**vite.config.ts**:
|
||
|
||
```ts
|
||
import { defineConfig } from "vite";
|
||
import react from "@vitejs/plugin-react";
|
||
|
||
export default defineConfig({
|
||
plugins: [react({ jsxRuntime: "classic" })],
|
||
build: {
|
||
lib: {
|
||
entry: "src/index.tsx",
|
||
formats: ["es"],
|
||
fileName: () => "index.js",
|
||
},
|
||
rollupOptions: { external: ["react", "react-dom"] },
|
||
},
|
||
});
|
||
```
|
||
|
||
`jsxRuntime: "classic"` compiles JSX to `React.createElement`, using the host-provided `React`; `external` avoids bundling React, using the version already loaded by the application.
|
||
|
||
#### Build and Install
|
||
|
||
```bash
|
||
npm install && npm run build
|
||
cp -r . ~/.qwenpaw/plugins/my-plugin/
|
||
qwenpaw app
|
||
```
|
||
|
||
You can copy `console/src/plugins/types/qwenpaw.d.ts` into your plugin project as `qwenpaw-host.d.ts` for full type hints.
|
||
|
||
## Frontend Extension API
|
||
|
||
Frontend plugins extend the Console UI through the `window.QwenPaw.*` API without modifying host code. All registration methods take `pluginId` as the first argument, and every registration returns a `{ dispose() }` object for revocation.
|
||
|
||
### Host SDK — `window.QwenPaw.host`
|
||
|
||
Shared dependencies — plugins do not need to bundle these libraries:
|
||
|
||
```ts
|
||
host.React // React library
|
||
host.ReactDOM // ReactDOM library
|
||
host.antd // Ant Design component library
|
||
host.antdIcons // Ant Design icons library
|
||
host.apiBaseUrl // API base URL
|
||
host.getApiUrl(path: string) // Build full API URL
|
||
host.getApiToken(): string | null // Get current auth token
|
||
```
|
||
|
||
**React Hooks (use inside React components):**
|
||
|
||
```ts
|
||
const theme = window.QwenPaw.host.useTheme(); // "light" | "dark"
|
||
const locale = window.QwenPaw.host.useLocale(); // "zh" | "en"
|
||
const agent = window.QwenPaw.host.useSelectedAgent(); // { id: string }
|
||
const session = window.QwenPaw.host.useCurrentSession(); // { id: string } | null
|
||
```
|
||
|
||
**Imperative getters (can be called anywhere):**
|
||
|
||
```ts
|
||
const agentId = window.QwenPaw.host.getSelectedAgentId();
|
||
const sessionId = window.QwenPaw.host.getCurrentSessionId();
|
||
```
|
||
|
||
**Authenticated fetch (automatically injects Authorization and X-Agent-Id headers):**
|
||
|
||
```ts
|
||
const resp = await window.QwenPaw.host.fetch("/api/v1/my-endpoint", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ query: "test" }),
|
||
});
|
||
const data = await resp.json();
|
||
```
|
||
|
||
### Sidebar Menu — `window.QwenPaw.menu`
|
||
|
||
| Method | Signature | Description |
|
||
| ---------- | ---------------------------------------- | ------------------------------------ |
|
||
| `add` | `(pluginId, item \| item[]): Disposable` | Add menu items |
|
||
| `replace` | `(pluginId, targetId, item): Disposable` | Replace an existing menu item |
|
||
| `remove` | `(targetId): void` | Remove a menu item |
|
||
| `snapshot` | `(location?): MenuItem[]` | Get a snapshot of current menu items |
|
||
|
||
**MenuItem Parameters:**
|
||
|
||
```ts
|
||
{
|
||
id: string; // Globally unique, e.g. "my-plugin.foo"
|
||
label: string | (() => ReactNode);
|
||
icon?: ReactComponent | ReactNode;
|
||
route?: string; // Route id to navigate to on click
|
||
parentId?: string; // Parent group to attach to
|
||
location?: "primary.agentScoped" | "primary.settings" | "userMenu";
|
||
before?: string; // Position before a specific id
|
||
after?: string; // Position after a specific id
|
||
order?: number; // Lower values appear first
|
||
visible?: () => boolean; // Dynamic visibility control
|
||
isGroup?: boolean; // Render as group header
|
||
divider?: boolean; // Render as horizontal divider
|
||
}
|
||
```
|
||
|
||
### Page Routes — `window.QwenPaw.route`
|
||
|
||
| Method | Signature | Description |
|
||
| --------- | --------------------------------------------- | -------------------------------------- |
|
||
| `add` | `(pluginId, route \| route[]): Disposable` | Register new routes |
|
||
| `replace` | `(pluginId, targetId, component): Disposable` | Replace an existing route's component |
|
||
| `wrap` | `(pluginId, targetId, wrapper): Disposable` | Wrap an existing route (onion pattern) |
|
||
| `remove` | `(targetId): void` | Remove a route |
|
||
|
||
**Route parameters:**
|
||
|
||
```ts
|
||
{
|
||
id: string; // Globally unique, e.g. "my-plugin.home"
|
||
path: string; // URL path, supports react-router patterns
|
||
component: React.ComponentType; // Page component
|
||
}
|
||
```
|
||
|
||
**Wrap example (add a top banner to an existing page):**
|
||
|
||
```tsx
|
||
window.QwenPaw.route.wrap("my-plugin", "core.chat", (Inner) => {
|
||
return () => (
|
||
<div>
|
||
<div style={{ background: "#fff3cd", padding: 8, textAlign: "center" }}>
|
||
Beta Feature
|
||
</div>
|
||
<Inner />
|
||
</div>
|
||
);
|
||
});
|
||
```
|
||
|
||
### General UI Slots — `window.QwenPaw.slot`
|
||
|
||
| Method | Signature | Description |
|
||
| ---------- | --------------------------------------------- | ------------------------------------------------------- |
|
||
| `fill` | `(pluginId, name, render, opts?): Disposable` | Append content to a slot (multiple can coexist) |
|
||
| `replace` | `(pluginId, name, render, opts?): Disposable` | Replace slot content (latest wins, overrides all fills) |
|
||
| `snapshot` | `(): SlotInfo[]` | Get all registered slot information |
|
||
|
||
**Built-in Slots:**
|
||
|
||
| Slot Name | Type | UI Location |
|
||
| ------------------- | ------- | ----------------------------------------- |
|
||
| `header.logo` | replace | Top navbar, leftmost |
|
||
| `header.left` | fill | Top navbar, left area (right of logo) |
|
||
| `header.right` | fill | Top navbar, right area (left of settings) |
|
||
| `sider.top` | fill | Sidebar top (below agent selector) |
|
||
| `sider.bottom` | fill | Sidebar bottom (below menu) |
|
||
| `content.statusBar` | fill | Main content area top |
|
||
| `overlay.global` | fill | Global overlay |
|
||
|
||
**Example:**
|
||
|
||
```tsx
|
||
// Replace Header Logo
|
||
window.QwenPaw.slot.replace("my-plugin", "header.logo", (defaultLogo) => {
|
||
return <img src="https://example.com/logo.svg" style={{ height: 24 }} />;
|
||
});
|
||
```
|
||
|
||
### Chat Welcome Screen — `chat.welcome`
|
||
|
||
```tsx
|
||
window.QwenPaw.chat.welcome.set("my-plugin", {
|
||
greeting: (locale) => (locale.startsWith("zh") ? "Hello!" : "Hello!"),
|
||
description: "I specialize in data analysis.",
|
||
avatar: "https://example.com/avatar.png",
|
||
nick: "My Bot",
|
||
prompts: [
|
||
{ label: "Analyze data", value: "Please analyze the uploaded dataset" },
|
||
{ label: "Create chart", value: "Create a bar chart from the data" },
|
||
],
|
||
});
|
||
|
||
// Or fully replace the welcome screen
|
||
window.QwenPaw.chat.welcome.render("my-plugin", (props) => {
|
||
return <div>Custom Welcome</div>;
|
||
});
|
||
```
|
||
|
||
### Chat Theme — `chat.theme`
|
||
|
||
```ts
|
||
window.QwenPaw.chat.theme.set("my-plugin", {
|
||
colorPrimary: "#1890ff",
|
||
});
|
||
```
|
||
|
||
### Chat Header — `chat.leftHeader` / `chat.rightHeader`
|
||
|
||
```tsx
|
||
// Set the left header title
|
||
window.QwenPaw.chat.leftHeader.set("my-plugin", {
|
||
title: "My Brand",
|
||
logo: <img src="logo.svg" style={{ height: 20 }} />,
|
||
});
|
||
|
||
// Add a button to the right header
|
||
window.QwenPaw.chat.rightHeader.add(
|
||
"my-plugin",
|
||
<button
|
||
onClick={() => alert("Plugin action!")}
|
||
style={{ border: "none", background: "none", cursor: "pointer" }}
|
||
>
|
||
My Button
|
||
</button>,
|
||
{ id: "my-plugin.btn", order: 10 },
|
||
);
|
||
```
|
||
|
||
### Input Box — `chat.sender`
|
||
|
||
```ts
|
||
// Custom placeholder
|
||
window.QwenPaw.chat.sender.set("my-plugin", {
|
||
placeholder: "Ask me anything...",
|
||
disclaimer: "Responses may not be accurate.",
|
||
});
|
||
|
||
// Add input suggestions
|
||
window.QwenPaw.chat.sender.addSuggestion("my-plugin", {
|
||
id: "my-plugin.suggestions",
|
||
items: [
|
||
{ label: "/analyze", value: "analyze" },
|
||
{ label: "/visualize", value: "visualize" },
|
||
],
|
||
});
|
||
```
|
||
|
||
### Message Action Buttons — `chat.actions` / `chat.requestActions`
|
||
|
||
```tsx
|
||
// Add action button below AI responses
|
||
window.QwenPaw.chat.actions.add("my-plugin", {
|
||
id: "my-plugin.star",
|
||
icon: <span>⭐</span>,
|
||
onClick: ({ data }) => console.log("Starred:", data),
|
||
});
|
||
|
||
// Add action button below user messages
|
||
window.QwenPaw.chat.requestActions.add("my-plugin", {
|
||
id: "my-plugin.edit",
|
||
icon: <span>✏️</span>,
|
||
onClick: ({ data }) => console.log("Edit:", data),
|
||
});
|
||
```
|
||
|
||
### Request Payload Transform — `chat.requestPayload`
|
||
|
||
Use `chat.requestPayload.add` to modify the outgoing chat request body before the Console sends it to the backend. Transforms run in ascending `order` and receive the current payload plus the resolved `sessionId` and `selectedAgent`.
|
||
|
||
```ts
|
||
window.QwenPaw.chat.requestPayload.add(
|
||
"my-plugin",
|
||
({ payload, sessionId, selectedAgent }) => ({
|
||
...payload,
|
||
request_context: {
|
||
session_id: sessionId,
|
||
agent_id: selectedAgent,
|
||
datasource_id: "ds-123",
|
||
},
|
||
}),
|
||
{ id: "my-plugin.request-context", order: 10 },
|
||
);
|
||
```
|
||
|
||
The transform may return a new object to replace the payload. Returning `undefined` leaves the payload unchanged. Use a globally unique `id` so the registration can be audited and disposed cleanly.
|
||
|
||
### Message Bubble Customization — `chat.request` / `chat.response`
|
||
|
||
```tsx
|
||
// Set the default assistant response avatar and nickname
|
||
// This currently reuses welcome.avatar / welcome.nick because the default ResponseCard reads those fields
|
||
window.QwenPaw.chat.response.set("my-plugin", {
|
||
avatar: "https://example.com/bot-avatar.png",
|
||
nick: "My Bot",
|
||
});
|
||
|
||
// Prepend content before user messages
|
||
window.QwenPaw.chat.request.prepend("my-plugin", ({ data }) => {
|
||
return <div style={{ fontSize: 10, color: "#999" }}>User</div>;
|
||
});
|
||
|
||
// Append an info bar below the latest AI response
|
||
window.QwenPaw.chat.response.append("my-plugin", ({ data, isLast }) => {
|
||
if (!isLast) return null;
|
||
return (
|
||
<div
|
||
style={{
|
||
background: "#e3f2fd",
|
||
padding: "4px 8px",
|
||
borderRadius: 4,
|
||
fontSize: 12,
|
||
}}
|
||
>
|
||
Powered by My Plugin
|
||
</div>
|
||
);
|
||
});
|
||
|
||
// Fully replace user message rendering (call fallback() to keep defaults)
|
||
window.QwenPaw.chat.request.render("my-plugin", ({ data, fallback }) => {
|
||
return (
|
||
<div style={{ border: "1px dashed #ccc", borderRadius: 8, padding: 4 }}>
|
||
{fallback()}
|
||
</div>
|
||
);
|
||
});
|
||
```
|
||
|
||
### Tool-Call Rendering — `chat.toolRender`
|
||
|
||
```tsx
|
||
// Register a custom tool result renderer (props include result, sessionId, messageId)
|
||
window.QwenPaw.chat.toolRender("my-plugin", "get_weather", ({ result }) => {
|
||
const data = typeof result === "string" ? JSON.parse(result) : result;
|
||
return (
|
||
<div style={{ padding: 12, border: "1px solid #e8e8e8", borderRadius: 8 }}>
|
||
{data.city}: {data.temperature}°C
|
||
</div>
|
||
);
|
||
});
|
||
```
|
||
|
||
### Custom Cards — `chat.card`
|
||
|
||
```ts
|
||
window.QwenPaw.chat.card("my-plugin", "my-card", MyCardComponent);
|
||
```
|
||
|
||
### Audit & Debugging
|
||
|
||
```ts
|
||
// View extension registration records
|
||
console.table(window.QwenPaw.audit.overrides());
|
||
|
||
// Remove all Chat extension registrations for a plugin
|
||
window.QwenPaw.chat.disposeAll("my-plugin");
|
||
```
|
||
|
||
### Internationalization
|
||
|
||
All fields that support the `Localized<T>` type accept a function that returns different values per locale:
|
||
|
||
```ts
|
||
window.QwenPaw.chat.welcome.set("my-plugin", {
|
||
greeting: (locale) => (locale.startsWith("zh") ? "Hello!" : "Hello!"),
|
||
});
|
||
```
|
||
|
||
### Common Errors
|
||
|
||
| Error | Cause | Solution |
|
||
| --------------------------------- | --------------------------------------------- | ------------------------------------------------------------------- |
|
||
| `e.item.render is not a function` | render/prepend/append received a non-function | Ensure you pass a React component or a function returning ReactNode |
|
||
| `duplicate id` | Two `add` calls used the same id | Use globally unique ids (recommended format: `pluginId.xxx`) |
|
||
| Hook called outside component | `useTheme()` etc. used outside React context | Use imperative APIs like `getSelectedAgentId()` instead |
|
||
|
||
## Usage Examples
|
||
|
||
### Example 1: Add Custom Provider
|
||
|
||
Let's say you want to connect to an enterprise internal LLM service.
|
||
|
||
#### 1. Create Plugin Directory
|
||
|
||
```bash
|
||
mkdir my-llm-provider
|
||
cd my-llm-provider
|
||
```
|
||
|
||
#### 2. Create plugin.json
|
||
|
||
```json
|
||
{
|
||
"id": "my-llm-provider",
|
||
"name": "My LLM Provider",
|
||
"version": "1.0.0",
|
||
"type": "provider",
|
||
"description": "Custom LLM provider for enterprise",
|
||
"author": "Your Name",
|
||
"entry": {
|
||
"backend": "plugin.py"
|
||
},
|
||
"dependencies": ["httpx>=0.24.0"],
|
||
"qwenpaw_version": {
|
||
"min": "1.0.0",
|
||
"max": "2.1.0"
|
||
},
|
||
"meta": {
|
||
"api_key_url": "https://example.com/get-api-key",
|
||
"api_key_hint": "Get your API key from example.com"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3. Create provider.py
|
||
|
||
```python
|
||
# -*- coding: utf-8 -*-
|
||
"""My LLM Provider Implementation."""
|
||
|
||
from qwenpaw.providers.openai_provider import OpenAIProvider
|
||
from qwenpaw.providers.provider import ModelInfo
|
||
from typing import List
|
||
|
||
|
||
class MyLLMProvider(OpenAIProvider):
|
||
"""My custom LLM provider (OpenAI-compatible)."""
|
||
|
||
def __init__(self, **kwargs):
|
||
"""Initialize provider."""
|
||
super().__init__(**kwargs)
|
||
|
||
@classmethod
|
||
def get_default_models(cls) -> List[ModelInfo]:
|
||
"""Get default models."""
|
||
return [
|
||
ModelInfo(
|
||
id="my-model-v1",
|
||
name="My Model V1",
|
||
supports_multimodal=False,
|
||
supports_image=False,
|
||
supports_video=False,
|
||
),
|
||
ModelInfo(
|
||
id="my-model-v2",
|
||
name="My Model V2",
|
||
supports_multimodal=True,
|
||
supports_image=True,
|
||
supports_video=False,
|
||
),
|
||
]
|
||
```
|
||
|
||
#### 4. Create plugin.py
|
||
|
||
```python
|
||
# -*- coding: utf-8 -*-
|
||
"""My LLM Provider Plugin Entry Point."""
|
||
|
||
import importlib.util
|
||
import logging
|
||
import os
|
||
|
||
from qwenpaw.plugins.api import PluginApi
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class MyLLMProviderPlugin:
|
||
"""My LLM Provider Plugin."""
|
||
|
||
def register(self, api: PluginApi):
|
||
"""Register the provider.
|
||
|
||
Args:
|
||
api: PluginApi instance
|
||
"""
|
||
logger.info("Registering My LLM Provider...")
|
||
|
||
# Load provider module from same directory
|
||
plugin_dir = os.path.dirname(os.path.abspath(__file__))
|
||
provider_path = os.path.join(plugin_dir, "provider.py")
|
||
|
||
spec = importlib.util.spec_from_file_location(
|
||
"my_provider", provider_path
|
||
)
|
||
provider_module = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(provider_module)
|
||
|
||
MyLLMProvider = provider_module.MyLLMProvider
|
||
|
||
# Register provider
|
||
api.register_provider(
|
||
provider_id="my-llm",
|
||
provider_class=MyLLMProvider,
|
||
label="My LLM",
|
||
base_url="https://api.example.com/v1",
|
||
)
|
||
|
||
logger.info("✓ My LLM Provider registered")
|
||
|
||
|
||
# Export plugin instance
|
||
plugin = MyLLMProviderPlugin()
|
||
```
|
||
|
||
#### 5. Install and Use
|
||
|
||
```bash
|
||
# Install plugin
|
||
qwenpaw plugin install my-llm-provider
|
||
|
||
# Start QwenPaw
|
||
qwenpaw app
|
||
```
|
||
|
||
### Example 2: Add Startup Hook
|
||
|
||
Let's say you want to initialize a monitoring service when QwenPaw starts.
|
||
|
||
#### 1. Create Plugin
|
||
|
||
```bash
|
||
mkdir monitoring-hook
|
||
cd monitoring-hook
|
||
```
|
||
|
||
#### 2. Create plugin.json
|
||
|
||
```json
|
||
{
|
||
"id": "monitoring-hook",
|
||
"name": "Monitoring Hook",
|
||
"version": "1.0.0",
|
||
"type": "hook",
|
||
"description": "Initialize monitoring service at startup",
|
||
"author": "Your Name",
|
||
"entry": {
|
||
"backend": "plugin.py"
|
||
},
|
||
"dependencies": [],
|
||
"qwenpaw_version": {
|
||
"min": "1.0.0",
|
||
"max": "2.1.0"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3. Create plugin.py
|
||
|
||
```python
|
||
# -*- coding: utf-8 -*-
|
||
"""Monitoring Hook Plugin Entry Point."""
|
||
|
||
from qwenpaw.plugins.api import PluginApi
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class MonitoringHookPlugin:
|
||
"""Monitoring Hook Plugin."""
|
||
|
||
def register(self, api: PluginApi):
|
||
"""Register the monitoring hook.
|
||
|
||
Args:
|
||
api: PluginApi instance
|
||
"""
|
||
logger.info("Registering monitoring hook...")
|
||
|
||
def startup_hook():
|
||
"""Startup hook to initialize monitoring."""
|
||
try:
|
||
logger.info("=== Monitoring Service Initialization ===")
|
||
|
||
# Initialize your monitoring service
|
||
# from my_monitoring import init_monitoring
|
||
# init_monitoring(app_name="QwenPaw")
|
||
|
||
logger.info("✓ Monitoring initialized successfully")
|
||
|
||
except Exception as e:
|
||
logger.error(
|
||
f"Failed to initialize monitoring: {e}",
|
||
exc_info=True,
|
||
)
|
||
|
||
# Register startup hook (priority=0 means highest priority)
|
||
api.register_startup_hook(
|
||
hook_name="monitoring_init",
|
||
callback=startup_hook,
|
||
priority=0,
|
||
)
|
||
|
||
logger.info("✓ Monitoring hook registered")
|
||
|
||
|
||
# Export plugin instance
|
||
plugin = MonitoringHookPlugin()
|
||
```
|
||
|
||
#### 4. Install
|
||
|
||
```bash
|
||
qwenpaw plugin install monitoring-hook
|
||
qwenpaw app
|
||
```
|
||
|
||
### Example 3: Add Custom Command
|
||
|
||
Let's say you want to add a `/status` command to check system status.
|
||
|
||
#### 1. Create Plugin
|
||
|
||
```bash
|
||
mkdir status-command
|
||
cd status-command
|
||
```
|
||
|
||
#### 2. Create plugin.json
|
||
|
||
```json
|
||
{
|
||
"id": "status-command",
|
||
"name": "Status Command",
|
||
"version": "1.0.0",
|
||
"type": "command",
|
||
"description": "Custom status command",
|
||
"author": "Your Name",
|
||
"entry": {
|
||
"backend": "plugin.py"
|
||
},
|
||
"dependencies": [],
|
||
"qwenpaw_version": {
|
||
"min": "1.0.0",
|
||
"max": "2.1.0"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3. Create plugin.py
|
||
|
||
```python
|
||
# -*- coding: utf-8 -*-
|
||
"""Status Command Plugin Entry Point."""
|
||
|
||
import logging
|
||
|
||
from qwenpaw.plugins.api import PluginApi
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class StatusCommandPlugin:
|
||
"""Status Command Plugin."""
|
||
|
||
def register(self, api: PluginApi):
|
||
"""Register the status command."""
|
||
from qwenpaw.runtime.commands.control.base import (
|
||
BaseControlCommandHandler,
|
||
)
|
||
|
||
class StatusCommandHandler(BaseControlCommandHandler):
|
||
command_name = "status"
|
||
help_text = "Check system status"
|
||
|
||
async def handle(self, ctx, args: str):
|
||
from agentscope.message import Msg
|
||
return Msg(
|
||
name="system",
|
||
role="assistant",
|
||
content="System is running normally.",
|
||
)
|
||
|
||
api.register_control_command(
|
||
handler=StatusCommandHandler(),
|
||
priority_level=10,
|
||
)
|
||
logger.info("✓ Status command registered: /status")
|
||
|
||
|
||
# Export plugin instance
|
||
plugin = StatusCommandPlugin()
|
||
```
|
||
|
||
#### 4. Install and Use
|
||
|
||
```bash
|
||
qwenpaw plugin install status-command
|
||
qwenpaw app
|
||
|
||
# Use the command
|
||
/status
|
||
```
|
||
|
||
### Example 4: Add a Custom Frontend Page
|
||
|
||
Add a welcome page to the sidebar. Build toolchain files (`package.json`, `tsconfig.json`, `vite.config.ts`) follow the "Frontend Plugins > Build Toolchain" section above.
|
||
|
||
**plugin.json**:
|
||
|
||
```json
|
||
{
|
||
"id": "welcome-plugin",
|
||
"name": "Welcome Plugin",
|
||
"version": "1.0.0",
|
||
"type": "frontend",
|
||
"description": "Welcome page plugin",
|
||
"author": "Your Name",
|
||
"entry": { "frontend": "dist/index.js" }
|
||
}
|
||
```
|
||
|
||
**src/index.tsx**:
|
||
|
||
```tsx
|
||
const { React, antd } = window.QwenPaw.host;
|
||
const { Typography, Card } = antd;
|
||
const pluginId = "welcome-plugin";
|
||
|
||
const WelcomePage = () => {
|
||
const theme = window.QwenPaw.host.useTheme();
|
||
return (
|
||
<Card
|
||
style={{
|
||
maxWidth: 480,
|
||
margin: "40px auto",
|
||
background: theme === "dark" ? "#1f1f1f" : "#fff",
|
||
}}
|
||
>
|
||
<Typography.Title level={2}>Welcome to QwenPaw</Typography.Title>
|
||
<Typography.Paragraph>Plugin system is working!</Typography.Paragraph>
|
||
</Card>
|
||
);
|
||
};
|
||
|
||
window.QwenPaw.menu.add(pluginId, {
|
||
id: "welcome-plugin.home",
|
||
label: "Welcome",
|
||
icon: "spark-home-line",
|
||
route: "welcome-plugin.home",
|
||
});
|
||
|
||
window.QwenPaw.route.add(pluginId, {
|
||
id: "welcome-plugin.home",
|
||
path: "/welcome-plugin/home",
|
||
component: WelcomePage,
|
||
});
|
||
```
|
||
|
||
```bash
|
||
npm install && npm run build
|
||
cp -r . ~/.qwenpaw/plugins/welcome-plugin/
|
||
qwenpaw app
|
||
```
|
||
|
||
### Example 5: Custom Tool-Call Renderer
|
||
|
||
Customize how Agent tool-call results are displayed. Project structure follows Example 4, only `src/index.tsx` differs.
|
||
|
||
**src/index.tsx**:
|
||
|
||
```tsx
|
||
const { React, antd } = window.QwenPaw.host;
|
||
const { Card, Descriptions } = antd;
|
||
const pluginId = "tool-render-plugin";
|
||
|
||
window.QwenPaw.chat.toolRender(pluginId, "get_weather", ({ result }) => {
|
||
const data = typeof result === "string" ? JSON.parse(result) : result;
|
||
return (
|
||
<Card
|
||
title="Weather Info"
|
||
size="small"
|
||
style={{ marginTop: 8, maxWidth: 400 }}
|
||
>
|
||
<Descriptions column={1} size="small">
|
||
<Descriptions.Item label="City">{data.city}</Descriptions.Item>
|
||
<Descriptions.Item label="Temperature">
|
||
{data.temperature}°C
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="Weather">{data.weather}</Descriptions.Item>
|
||
</Descriptions>
|
||
</Card>
|
||
);
|
||
});
|
||
```
|
||
|
||
### Example 6: Customize Chat Welcome
|
||
|
||
Customize the chat page greeting, description, and suggested prompts. Project structure follows Example 4, only `src/index.tsx` differs.
|
||
|
||
**src/index.tsx**:
|
||
|
||
```tsx
|
||
const pluginId = "custom-greeting-plugin";
|
||
|
||
window.QwenPaw.chat.welcome.set(pluginId, {
|
||
greeting: (locale) =>
|
||
locale.startsWith("zh")
|
||
? "Hello! I'm customized QwenPaw"
|
||
: "Hello! I'm customized QwenPaw",
|
||
description: "This is a customized chat assistant",
|
||
prompts: [
|
||
{ label: "Analyze code", value: "Help me analyze this code" },
|
||
{ label: "Unit test", value: "Write a unit test" },
|
||
{ label: "Optimize", value: "Optimize this logic" },
|
||
],
|
||
});
|
||
```
|
||
|
||
### Example 7: Expose a FastAPI Endpoint
|
||
|
||
Backend plugins can expose their own HTTP endpoints by registering a
|
||
`fastapi.APIRouter`. The router is mounted under `/api` + your prefix
|
||
and is served by the same FastAPI app as QwenPaw's core API, so it
|
||
shares CORS settings, the auth layer, and is included in
|
||
`/openapi.json` / `/docs`.
|
||
|
||
In this example we add a small `/api/pets` endpoint that returns a
|
||
list of pets and lets the user add new ones.
|
||
|
||
#### 1. Create plugin directory
|
||
|
||
```bash
|
||
mkdir pet-api-plugin && cd pet-api-plugin
|
||
```
|
||
|
||
#### 2. Create plugin.json
|
||
|
||
```json
|
||
{
|
||
"id": "pet-api-plugin",
|
||
"name": "Pet API Plugin",
|
||
"version": "1.0.0",
|
||
"type": "general",
|
||
"description": "Expose a small REST API under /api/pets",
|
||
"author": "Your Name",
|
||
"entry": {
|
||
"backend": "plugin.py"
|
||
},
|
||
"dependencies": [],
|
||
"qwenpaw_version": {
|
||
"min": "1.1.5",
|
||
"max": "2.1.0"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3. Create plugin.py
|
||
|
||
```python
|
||
# -*- coding: utf-8 -*-
|
||
"""Pet API Plugin Entry Point."""
|
||
|
||
import logging
|
||
from typing import List
|
||
|
||
from fastapi import APIRouter, HTTPException
|
||
from pydantic import BaseModel
|
||
|
||
from qwenpaw.plugins.api import PluginApi
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class Pet(BaseModel):
|
||
"""Pet model."""
|
||
|
||
id: int
|
||
name: str
|
||
species: str
|
||
|
||
|
||
class PetCreate(BaseModel):
|
||
"""Pet creation payload."""
|
||
|
||
name: str
|
||
species: str
|
||
|
||
|
||
_PETS: List[Pet] = [
|
||
Pet(id=1, name="Mochi", species="cat"),
|
||
Pet(id=2, name="Bao", species="dog"),
|
||
]
|
||
|
||
|
||
def build_router() -> APIRouter:
|
||
"""Build the plugin's APIRouter.
|
||
|
||
Routes are mounted under ``/api`` + the prefix passed to
|
||
``register_http_router``. With ``prefix="/pets"`` the handlers
|
||
below are served at ``/api/pets`` and ``/api/pets/{pet_id}``.
|
||
"""
|
||
router = APIRouter()
|
||
|
||
@router.get("", response_model=List[Pet])
|
||
def list_pets() -> List[Pet]:
|
||
"""Return all pets."""
|
||
return list(_PETS)
|
||
|
||
@router.get("/{pet_id}", response_model=Pet)
|
||
def get_pet(pet_id: int) -> Pet:
|
||
"""Return a single pet by id."""
|
||
for pet in _PETS:
|
||
if pet.id == pet_id:
|
||
return pet
|
||
raise HTTPException(status_code=404, detail="Pet not found")
|
||
|
||
@router.post("", response_model=Pet, status_code=201)
|
||
def create_pet(payload: PetCreate) -> Pet:
|
||
"""Create a new pet."""
|
||
new_id = (max((p.id for p in _PETS), default=0)) + 1
|
||
pet = Pet(id=new_id, name=payload.name, species=payload.species)
|
||
_PETS.append(pet)
|
||
return pet
|
||
|
||
return router
|
||
|
||
|
||
class PetApiPlugin:
|
||
"""Pet API Plugin."""
|
||
|
||
def register(self, api: PluginApi):
|
||
"""Register the HTTP router.
|
||
|
||
Args:
|
||
api: PluginApi instance
|
||
"""
|
||
logger.info("Registering Pet API plugin...")
|
||
|
||
api.register_http_router(
|
||
build_router(),
|
||
prefix="/pets",
|
||
tags=["pets"],
|
||
)
|
||
|
||
logger.info("✓ Pet API registered at /api/pets")
|
||
|
||
|
||
# Export plugin instance
|
||
plugin = PetApiPlugin()
|
||
```
|
||
|
||
#### 4. Install and try it out
|
||
|
||
```bash
|
||
qwenpaw plugin install pet-api-plugin
|
||
```
|
||
|
||
Once QwenPaw is running:
|
||
|
||
```bash
|
||
# List pets
|
||
curl http://127.0.0.1:8088/api/pets
|
||
|
||
# Get one pet
|
||
curl http://127.0.0.1:8088/api/pets/1
|
||
|
||
# Create a pet
|
||
curl -X POST http://127.0.0.1:8088/api/pets \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"name": "Luna", "species": "rabbit"}'
|
||
```
|
||
|
||
**Notes:**
|
||
|
||
- `prefix` must start with `/` and must not be just `/` — use a
|
||
descriptive segment such as `/pets`. The full URL is always
|
||
`/api` + your prefix.
|
||
- Each prefix can only be claimed by one plugin. Registering the
|
||
same prefix twice raises `ValueError`.
|
||
- `tags` is optional; when omitted, routes are tagged
|
||
`plugin:<plugin_id>` automatically for OpenAPI grouping.
|
||
- Routes are unmounted automatically when the plugin is uninstalled
|
||
or disabled.
|
||
|
||
### Example 8: Tracing Middleware (Tool Call Tracing)
|
||
|
||
This example demonstrates how to register an `on_acting` middleware that logs every tool call with timing information when the `QWENPAW_TRACE` environment variable is set.
|
||
|
||
**plugin.json:**
|
||
|
||
```json
|
||
{
|
||
"id": "middleware-demo-tracing",
|
||
"name": "Tracing Middleware Demo",
|
||
"version": "1.0.0",
|
||
"description": "Demo: logs tool calls with execution timing to a trace file",
|
||
"author": "QwenPaw Team",
|
||
"type": "general",
|
||
"entry": {
|
||
"backend": "tracing_plugin.py"
|
||
},
|
||
"dependencies": [],
|
||
"qwenpaw_version": {
|
||
"min": "1.0.0",
|
||
"max": "2.1.0"
|
||
}
|
||
}
|
||
```
|
||
|
||
**tracing_plugin.py:**
|
||
|
||
```python
|
||
import os
|
||
import time
|
||
from pathlib import Path
|
||
from typing import Any, AsyncGenerator, Callable
|
||
|
||
from agentscope.middleware import MiddlewareBase
|
||
from qwenpaw.plugins.api import PluginApi
|
||
|
||
|
||
class TracingMiddleware(MiddlewareBase):
|
||
"""Logs tool call name, input, and execution duration."""
|
||
|
||
def __init__(self, trace_file: Path) -> None:
|
||
self._trace_file = trace_file
|
||
self._trace_file.parent.mkdir(parents=True, exist_ok=True)
|
||
|
||
async def on_acting(
|
||
self,
|
||
agent: Any,
|
||
input_kwargs: dict[str, Any],
|
||
next_handler: Callable[..., AsyncGenerator[Any, None]],
|
||
) -> AsyncGenerator[Any, None]:
|
||
tool_call = input_kwargs["tool_call"]
|
||
tool_name = getattr(tool_call, "name", str(tool_call))
|
||
tool_input = getattr(tool_call, "input", "")
|
||
|
||
start = time.perf_counter()
|
||
try:
|
||
async for item in next_handler():
|
||
yield item
|
||
finally:
|
||
elapsed_ms = (time.perf_counter() - start) * 1000
|
||
line = f"[{time.strftime('%H:%M:%S')}] {tool_name}({tool_input[:100]}) — {elapsed_ms:.1f}ms\n"
|
||
with open(self._trace_file, "a", encoding="utf-8") as f:
|
||
f.write(line)
|
||
|
||
|
||
def _tracing_factory(ctx: Any, agent_config: Any) -> TracingMiddleware | None:
|
||
"""Create TracingMiddleware when QWENPAW_TRACE env var is set."""
|
||
if not os.environ.get("QWENPAW_TRACE"):
|
||
return None
|
||
workspace_dir = getattr(ctx, "workspace_dir", None)
|
||
if workspace_dir is None:
|
||
return None
|
||
trace_file = Path(workspace_dir) / ".qwenpaw" / "trace.log"
|
||
return TracingMiddleware(trace_file=trace_file)
|
||
|
||
|
||
class TracingPlugin:
|
||
def register(self, api: PluginApi) -> None:
|
||
api.register_middleware(_tracing_factory, priority=50)
|
||
|
||
|
||
plugin = TracingPlugin()
|
||
```
|
||
|
||
**Key points:**
|
||
|
||
- **Conditional activation**: The factory checks the `QWENPAW_TRACE` environment variable and only activates when set
|
||
- **`priority=50`**: Higher priority (lower number = outermost in onion), ensuring tracing wraps other middlewares
|
||
- **`on_acting` hook**: Measures execution time before/after tool calls
|
||
- Full source: `plugins/middleware-demo/tracing-middleware/tracing_plugin.py`
|
||
|
||
---
|
||
|
||
### Example 9: Thinking Log Middleware (Reasoning Process Logger)
|
||
|
||
This example demonstrates how to register an `on_reasoning` middleware that captures and prints the model's chain-of-thought.
|
||
|
||
**plugin.json:**
|
||
|
||
```json
|
||
{
|
||
"id": "middleware-demo-thinking-log",
|
||
"name": "Thinking Log Middleware Demo",
|
||
"version": "1.0.0",
|
||
"description": "Demo: prints model reasoning steps to stdout",
|
||
"author": "QwenPaw Team",
|
||
"type": "general",
|
||
"entry": {
|
||
"backend": "thinking_log_plugin.py"
|
||
},
|
||
"dependencies": [],
|
||
"qwenpaw_version": {
|
||
"min": "1.0.0",
|
||
"max": "2.1.0"
|
||
}
|
||
}
|
||
```
|
||
|
||
**thinking_log_plugin.py:**
|
||
|
||
```python
|
||
import sys
|
||
from typing import Any, AsyncGenerator, Callable
|
||
|
||
from agentscope.middleware import MiddlewareBase
|
||
from agentscope.event import ThinkingBlockDeltaEvent, TextBlockDeltaEvent
|
||
from qwenpaw.plugins.api import PluginApi
|
||
|
||
|
||
class ThinkingLogMiddleware(MiddlewareBase):
|
||
"""Prints reasoning stream events to stdout."""
|
||
|
||
async def on_reasoning(
|
||
self,
|
||
agent: Any,
|
||
input_kwargs: dict[str, Any],
|
||
next_handler: Callable[..., AsyncGenerator[Any, None]],
|
||
) -> AsyncGenerator[Any, None]:
|
||
async for item in next_handler():
|
||
if isinstance(item, ThinkingBlockDeltaEvent):
|
||
print(f"[THINKING] {item.delta}", end="", file=sys.stdout, flush=True)
|
||
elif isinstance(item, TextBlockDeltaEvent):
|
||
print(f"[TEXT] {item.delta}", end="", file=sys.stdout, flush=True)
|
||
yield item
|
||
|
||
|
||
def _thinking_log_factory(ctx: Any, agent_config: Any) -> ThinkingLogMiddleware:
|
||
"""Always create the middleware (unconditional activation)."""
|
||
return ThinkingLogMiddleware()
|
||
|
||
|
||
class ThinkingLogPlugin:
|
||
def register(self, api: PluginApi) -> None:
|
||
api.register_middleware(_thinking_log_factory, priority=80)
|
||
|
||
|
||
plugin = ThinkingLogPlugin()
|
||
```
|
||
|
||
**Key points:**
|
||
|
||
- **Unconditional activation**: The factory always returns an instance, applied to every request
|
||
- **`on_reasoning` hook**: Captures streaming events during the model's reasoning phase (`ThinkingBlockDeltaEvent` for chain-of-thought, `TextBlockDeltaEvent` for text responses)
|
||
- **Real-time printing**: Each delta event is printed immediately while being yielded downstream — does not block streaming
|
||
- Full source: `plugins/middleware-demo/thinking-log-middleware/thinking_log_plugin.py`
|
||
|
||
---
|
||
|
||
### Example 10: Register a Custom Channel
|
||
|
||
Channel plugins let you add new messaging platforms to QwenPaw. The channel
|
||
appears in the Console UI alongside built-in channels (DingTalk, Telegram,
|
||
etc.) and can be configured, enabled, and disabled the same way.
|
||
|
||
#### 1. Create Plugin Directory
|
||
|
||
```bash
|
||
mkdir sample-channel-plugin && cd sample-channel-plugin
|
||
```
|
||
|
||
#### 2. Create plugin.json
|
||
|
||
```json
|
||
{
|
||
"id": "sample-channel",
|
||
"name": "Sample Channel",
|
||
"version": "1.0.0",
|
||
"type": "channel",
|
||
"description": "Sample messaging channel integration for QwenPaw",
|
||
"author": "Your Name",
|
||
"entry": {
|
||
"backend": "plugin.py"
|
||
},
|
||
"dependencies": ["sample-sdk>=1.0.0"],
|
||
"qwenpaw_version": {
|
||
"min": "1.1.5",
|
||
"max": "2.1.0"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3. Create channel.py — BaseChannel subclass
|
||
|
||
Your channel class must implement the `BaseChannel` contract. The key
|
||
methods are:
|
||
|
||
- **`from_config(cls, process, config, ...)`** — classmethod that creates
|
||
an instance from saved configuration. This is how `ChannelManager`
|
||
instantiates your channel at startup.
|
||
- **`start()` / `stop()`** — lifecycle hooks called when the channel is
|
||
enabled/disabled.
|
||
- **`send(to_handle, text, meta)`** — send a message to a user/session.
|
||
|
||
```python
|
||
# -*- coding: utf-8 -*-
|
||
"""Sample channel implementation."""
|
||
|
||
import logging
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
|
||
from qwenpaw.app.channels.base import (
|
||
BaseChannel,
|
||
OnReplySent,
|
||
ProcessHandler,
|
||
)
|
||
from qwenpaw.app.channels.renderer import ChannelDisplayConfig
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class SampleChannel(BaseChannel):
|
||
"""Sample messaging channel."""
|
||
|
||
channel = "sample" # unique key, must match config key
|
||
|
||
def __init__(
|
||
self,
|
||
process: ProcessHandler,
|
||
enabled: bool = True,
|
||
bot_token: str = "",
|
||
signing_secret: str = "",
|
||
bot_prefix: str = "",
|
||
on_reply_sent: OnReplySent = None,
|
||
display_config: ChannelDisplayConfig | None = None,
|
||
**kwargs,
|
||
):
|
||
super().__init__(
|
||
process,
|
||
on_reply_sent=on_reply_sent,
|
||
display_config=display_config,
|
||
)
|
||
self.enabled = enabled
|
||
self.bot_prefix = bot_prefix
|
||
self.bot_token = bot_token
|
||
self.signing_secret = signing_secret
|
||
|
||
@classmethod
|
||
def from_config(
|
||
cls,
|
||
process: ProcessHandler,
|
||
config,
|
||
on_reply_sent: OnReplySent = None,
|
||
display_config: ChannelDisplayConfig | None = None,
|
||
workspace_dir: Optional[Path] = None,
|
||
) -> "SampleChannel":
|
||
"""Create from config.
|
||
|
||
Note: for plugin channels, ``config`` is a
|
||
``types.SimpleNamespace`` object (not a dict). Use
|
||
``getattr(config, "field", default)`` to read fields safely.
|
||
"""
|
||
return cls(
|
||
process=process,
|
||
enabled=getattr(config, "enabled", False),
|
||
bot_token=getattr(config, "bot_token", ""),
|
||
signing_secret=getattr(config, "signing_secret", ""),
|
||
bot_prefix=getattr(config, "bot_prefix", ""),
|
||
on_reply_sent=on_reply_sent,
|
||
display_config=display_config
|
||
or ChannelDisplayConfig.from_config(config),
|
||
)
|
||
|
||
async def start(self):
|
||
"""Start the sample event listener."""
|
||
logger.info("Sample channel starting (token=%s...)", self.bot_token[:8])
|
||
# Start your platform's API client here
|
||
|
||
async def stop(self):
|
||
"""Stop the sample event listener."""
|
||
logger.info("Sample channel stopping")
|
||
|
||
async def send(self, to_handle: str, text: str, meta=None):
|
||
"""Send a message to a sample user or channel."""
|
||
logger.info("Sending to sample %s: %s", to_handle, text[:50])
|
||
# Use sample-sdk to post messages
|
||
```
|
||
|
||
> **Important: `config` parameter type** — For plugin channels, the
|
||
> `config` passed to `from_config()` is a `types.SimpleNamespace` object
|
||
> (not a dict or Pydantic model). The framework merges
|
||
> `BaseChannelConfig` defaults with the user's saved config before
|
||
> passing it. Always use `getattr(config, "field", default)` to read
|
||
> fields safely.
|
||
|
||
#### 4. Create plugin.py — Plugin entry point
|
||
|
||
```python
|
||
# -*- coding: utf-8 -*-
|
||
"""Sample Channel Plugin Entry Point."""
|
||
|
||
import logging
|
||
from qwenpaw.plugins.api import PluginApi
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class SampleChannelPlugin:
|
||
"""Sample Channel Plugin."""
|
||
|
||
def register(self, api: PluginApi):
|
||
"""Register the sample channel."""
|
||
from .channel import SampleChannel
|
||
|
||
api.register_channel(
|
||
channel_class=SampleChannel,
|
||
label="Sample",
|
||
description="Sample messaging channel integration",
|
||
icon="https://example.com/sample-icon.png", # optional card icon (http/https only)
|
||
doc_url={ # optional doc link, plain string or localized dict (http/https only)
|
||
"zh": "https://example.com/docs?lang=zh",
|
||
"en": "https://example.com/docs?lang=en",
|
||
},
|
||
config_fields=[
|
||
{
|
||
"name": "bot_token",
|
||
"label": "Bot Token",
|
||
"type": "password",
|
||
"required": True,
|
||
"placeholder": "your-bot-token-here",
|
||
"help": "Bot access token",
|
||
},
|
||
{
|
||
"name": "signing_secret",
|
||
"label": "Signing Secret",
|
||
"type": "password",
|
||
"required": True,
|
||
"help": "Signing secret for request verification",
|
||
},
|
||
{
|
||
"name": "streaming_enabled",
|
||
"label": {
|
||
"zh-CN": "流式输出",
|
||
"en-US": "Streaming Output",
|
||
},
|
||
"type": "switch",
|
||
"required": False,
|
||
"default": False,
|
||
},
|
||
],
|
||
)
|
||
logger.info("✓ Sample channel registered")
|
||
|
||
|
||
plugin = SampleChannelPlugin()
|
||
```
|
||
|
||
#### 5. Install and Use
|
||
|
||
```bash
|
||
qwenpaw plugin install sample-channel-plugin
|
||
qwenpaw app
|
||
```
|
||
|
||
After starting, go to **Control → Channels** in the Console. The sample
|
||
channel card will appear alongside built-in channels. Click it to fill in
|
||
credentials and enable it.
|
||
|
||
#### 6. Adding Webhook Endpoints (Optional)
|
||
|
||
If your channel needs to receive HTTP callbacks (e.g. your platform's
|
||
events API), register a FastAPI router in the same plugin:
|
||
|
||
```python
|
||
from fastapi import APIRouter
|
||
|
||
def register(self, api: PluginApi):
|
||
from .channel import SampleChannel
|
||
|
||
api.register_channel(channel_class=SampleChannel, ...)
|
||
|
||
# Mount webhook endpoint at /api/sample/events
|
||
router = APIRouter()
|
||
|
||
@router.post("/events")
|
||
async def sample_events(request):
|
||
body = await request.json()
|
||
# Handle event verification and messages
|
||
return {"ok": True}
|
||
|
||
api.register_http_router(router, prefix="/sample", tags=["sample"])
|
||
```
|
||
|
||
**Key points:**
|
||
|
||
- The `channel_class` must be a `BaseChannel` subclass with a `channel`
|
||
class attribute (the unique key).
|
||
- **You must implement `from_config`** — this is how `ChannelManager`
|
||
creates your channel at startup. The `config` parameter is a
|
||
`SimpleNamespace`, not a dict.
|
||
- `config_fields` defines the form fields shown in the Console settings
|
||
drawer. Supported types: `text`, `password`, `number`, `switch`, `select`.
|
||
- The `label`, `help`, and `placeholder` of each field accept either a
|
||
plain string or a localized dict. Dict keys support **both long codes
|
||
(e.g. `zh-CN`, `en-US`) and short codes (e.g. `zh`, `en`), which can be
|
||
mixed freely**. The value is resolved with fallback in order (exact locale
|
||
→ short code → short-code prefix match → English → Chinese → first
|
||
non-empty value) so a missing locale never renders blank.
|
||
- `icon` (optional) is a custom icon URL for the channel card. Only
|
||
`http`/`https` URLs are supported; other values are ignored and fall back
|
||
to the default icon.
|
||
- `doc_url` (optional) is a documentation link for the channel. It can be a
|
||
plain string or a localized dict (e.g. `{"zh": "...", "en": "..."}`, same
|
||
long/short code rules as `label`). Only `http`/`https` URLs are supported;
|
||
the Console shows a "Doc" button in the settings drawer header that opens
|
||
the link for the current language, and hides it when the value is invalid
|
||
or missing.
|
||
- Plugin channels share the same enable/disable, access control, and
|
||
`bot_prefix` features as built-in channels.
|
||
- If a plugin channel key conflicts with a built-in key, the built-in
|
||
takes precedence and the plugin channel is skipped with a warning.
|
||
- For webhook-based channels, combine `register_channel` with
|
||
`register_http_router` in the same plugin.
|
||
|
||
## Dependency Management
|
||
|
||
### Using requirements.txt
|
||
|
||
If your plugin requires additional Python packages, create `requirements.txt`:
|
||
|
||
```
|
||
httpx>=0.24.0
|
||
pydantic>=2.0.0
|
||
```
|
||
|
||
Dependencies will be automatically installed when the plugin is installed.
|
||
|
||
### Using Custom PyPI Index
|
||
|
||
```
|
||
--index-url https://custom-pypi.example.com/simple
|
||
my-package>=1.0.0
|
||
```
|
||
|
||
## Best Practices
|
||
|
||
### 1. Naming Conventions
|
||
|
||
- **Plugin ID**: Use lowercase letters and hyphens, e.g., `my-plugin`
|
||
- **Version**: Follow semantic versioning (1.0.0, 1.1.0, 2.0.0)
|
||
|
||
### 2. Error Handling
|
||
|
||
Hook callbacks should handle errors gracefully to avoid blocking application startup:
|
||
|
||
```python
|
||
def startup_hook():
|
||
try:
|
||
# Your initialization code
|
||
pass
|
||
except Exception as e:
|
||
logger.error(f"Initialization failed: {e}", exc_info=True)
|
||
# Don't raise, let the application continue
|
||
```
|
||
|
||
### 3. Logging
|
||
|
||
Use Python logging to record plugin behavior:
|
||
|
||
```python
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
logger.info("Plugin loaded")
|
||
logger.debug("Debug information")
|
||
logger.error("Error occurred", exc_info=True)
|
||
```
|
||
|
||
### 4. Documentation
|
||
|
||
Provide clear README.md documentation including:
|
||
|
||
- Feature description
|
||
- Installation steps
|
||
- Usage examples
|
||
- Configuration instructions
|
||
- Troubleshooting
|
||
|
||
## Priority System
|
||
|
||
### Hook Priority
|
||
|
||
Hooks are executed in priority order:
|
||
|
||
- **Lower priority values execute earlier**
|
||
- Priority 0 = Highest priority (executes first)
|
||
- Priority 100 = Default priority
|
||
- Priority 200 = Low priority (executes last)
|
||
|
||
**Example**:
|
||
|
||
```python
|
||
# Executes first
|
||
api.register_startup_hook("early", callback, priority=0)
|
||
|
||
# Default order
|
||
api.register_startup_hook("normal", callback, priority=100)
|
||
|
||
# Executes last
|
||
api.register_startup_hook("late", callback, priority=200)
|
||
```
|
||
|
||
## Troubleshooting
|
||
|
||
### Plugin Not Loading
|
||
|
||
1. Check if plugin is installed:
|
||
|
||
```bash
|
||
qwenpaw plugin list
|
||
```
|
||
|
||
2. View QwenPaw logs:
|
||
|
||
```bash
|
||
tail -f ~/.qwenpaw/logs/qwenpaw.log | grep -i plugin
|
||
```
|
||
|
||
3. Verify plugin manifest format:
|
||
```bash
|
||
qwenpaw plugin info <plugin-id>
|
||
```
|
||
|
||
### Dependency Installation Failed
|
||
|
||
1. Check `requirements.txt` format
|
||
2. Manually test dependency installation:
|
||
```bash
|
||
pip install -r /path/to/plugin/requirements.txt
|
||
```
|
||
3. Reinstall plugin with `--force` flag
|
||
|
||
### Provider Not Showing
|
||
|
||
1. Confirm plugin is installed and restart QwenPaw
|
||
2. Check the model management page in Web UI
|
||
3. Review provider registration info in logs
|
||
|
||
### Command Not Responding
|
||
|
||
1. Confirm plugin is installed
|
||
2. Check if the command handler was registered successfully in logs
|
||
3. Verify the command name matches (e.g. `/status`)
|
||
|
||
## Security Considerations
|
||
|
||
1. **Only install trusted plugins**: Plugin code executes in the QwenPaw process
|
||
2. **Check dependencies**: Ensure plugin dependencies come from trusted sources
|
||
3. **Review code**: Review plugin source code before installation
|
||
4. **Hot-loading awareness**: The current version supports hot-installing/uninstalling plugins via API while the app is running. Be mindful of state consistency during hot-loading
|
||
|
||
## PluginApi Reference
|
||
|
||
### register_provider
|
||
|
||
Register a custom LLM provider.
|
||
|
||
```python
|
||
api.register_provider(
|
||
provider_id: str, # Unique provider identifier (required)
|
||
provider_class: Type, # Provider class (required)
|
||
label: str = "", # Display name (optional, defaults to provider_id)
|
||
base_url: str = "", # API base URL (optional)
|
||
**metadata, # Additional keyword args (chat_model, require_api_key, etc.)
|
||
)
|
||
```
|
||
|
||
### register_startup_hook
|
||
|
||
Register a startup hook.
|
||
|
||
```python
|
||
api.register_startup_hook(
|
||
hook_name: str, # Hook name
|
||
callback: Callable, # Callback function
|
||
priority: int = 100, # Priority (lower = earlier)
|
||
)
|
||
```
|
||
|
||
### register_shutdown_hook
|
||
|
||
Register a shutdown hook.
|
||
|
||
```python
|
||
api.register_shutdown_hook(
|
||
hook_name: str, # Hook name
|
||
callback: Callable, # Callback function
|
||
priority: int = 100, # Priority (lower = earlier)
|
||
)
|
||
```
|
||
|
||
### register_http_router
|
||
|
||
Mount a `fastapi.APIRouter` under `/api` + _prefix_.
|
||
|
||
```python
|
||
api.register_http_router(
|
||
router: APIRouter, # fastapi.APIRouter instance
|
||
*,
|
||
prefix: str, # Path under /api, e.g. "/pets"
|
||
tags: Optional[List[str]] = None, # OpenAPI tags (optional)
|
||
)
|
||
```
|
||
|
||
See [Example 7](#example-7-expose-a-fastapi-endpoint) for a full
|
||
walkthrough.
|
||
|
||
### register_control_command
|
||
|
||
Register a custom `/slash` control command.
|
||
|
||
```python
|
||
api.register_control_command(
|
||
handler: BaseControlCommandHandler, # Command handler instance
|
||
priority_level: int = 10, # Command priority (default: 10)
|
||
)
|
||
```
|
||
|
||
The handler must inherit from `qwenpaw.runtime.commands.control.base.BaseControlCommandHandler` and implement `command_name`, `help_text`, and `async handle(self, ctx, args)`.
|
||
|
||
### register_tool
|
||
|
||
Register a tool function into the Agent's toolkit.
|
||
|
||
```python
|
||
api.register_tool(
|
||
tool_name: str, # Unique tool function name
|
||
tool_func: Callable, # The tool callable to register
|
||
description: str = "", # Human-readable description shown in the UI
|
||
icon: str = "🔧", # Display icon (emoji string)
|
||
enabled: bool = False, # Whether the tool is enabled by default
|
||
)
|
||
```
|
||
|
||
### register_uninstall_hook
|
||
|
||
Register a hook that runs only when the plugin is explicitly uninstalled.
|
||
|
||
```python
|
||
api.register_uninstall_hook(
|
||
hook_name: str, # Hook name
|
||
callback: Callable, # Callback function
|
||
priority: int = 100, # Priority (lower = earlier)
|
||
)
|
||
```
|
||
|
||
### register_workspace_created_hook
|
||
|
||
Register a hook that fires when a new workspace is created.
|
||
|
||
```python
|
||
api.register_workspace_created_hook(
|
||
hook_name: str, # Hook name
|
||
callback: Callable, # Callback: (workspace_info: dict) -> None
|
||
priority: int = 100, # Priority (lower = earlier)
|
||
)
|
||
```
|
||
|
||
### get_tool_config / set_tool_config
|
||
|
||
Get or save per-agent tool configuration.
|
||
|
||
```python
|
||
config = api.get_tool_config(tool_name: str, agent_id: str) # Returns dict
|
||
api.set_tool_config(tool_name: str, agent_id: str, config: dict)
|
||
```
|
||
|
||
### register_middleware
|
||
|
||
Register an AgentScope `MiddlewareBase` factory.
|
||
|
||
```python
|
||
api.register_middleware(
|
||
middleware_factory: Callable, # Factory function
|
||
*,
|
||
priority: int = 100, # Priority (lower = outermost)
|
||
)
|
||
```
|
||
|
||
Factory signature: `(ctx: HookContext, agent_config: AgentProfileConfig) -> MiddlewareBase | None`
|
||
|
||
- `ctx` contains request-level context such as `session_id`, `agent_id`, `workspace_dir`
|
||
- Returning `None` means this middleware is skipped for the current request
|
||
- Lower `priority` values place the middleware further out in the onion model (executed first)
|
||
|
||
The factory is called during `AgentBuilder.build()` for each request. The returned middleware instance is inserted into the agent's middleware chain.
|
||
|
||
See [Example 8](#example-8-tracing-middleware) and [Example 9](#example-9-thinking-log-middleware) above for full walkthroughs.
|
||
|
||
## Advanced Features
|
||
|
||
### Modifying Agent Behavior
|
||
|
||
To intercept or enhance agent request processing, use one of these approaches:
|
||
|
||
- **Enhance the agent reasoning loop**: use `register_middleware` to inject AgentScope middlewares (`on_acting` / `on_reasoning` hooks)
|
||
- **Intercept specific commands**: use `register_control_command` to register a custom command handler
|
||
- **Inject logic into the request lifecycle**: use `HookRegistry` (8-phase hooks)
|
||
|
||
The current request flow is `Runtime.run()` → `AgentBuilder.build()` → `AgentExecutor.run()`.
|
||
|
||
### Custom Commands
|
||
|
||
In 2.0, the recommended way to add custom `/slash` commands is via `api.register_control_command()`. This replaces the old monkey patching approach:
|
||
|
||
```python
|
||
from qwenpaw.runtime.commands.control.base import BaseControlCommandHandler
|
||
|
||
class MyCommandHandler(BaseControlCommandHandler):
|
||
command_name = "mycommand"
|
||
help_text = "Description of my command"
|
||
|
||
async def handle(self, ctx, args: str):
|
||
from agentscope.message import Msg
|
||
return Msg(
|
||
name="system",
|
||
role="assistant",
|
||
content="Command result here.",
|
||
)
|
||
|
||
api.register_control_command(
|
||
handler=MyCommandHandler(),
|
||
priority_level=10,
|
||
)
|
||
```
|
||
|
||
### Access Runtime Information
|
||
|
||
Access runtime information through `api.runtime`:
|
||
|
||
```python
|
||
def my_hook():
|
||
# Access provider manager
|
||
provider_manager = api.runtime.provider_manager
|
||
|
||
# Get all providers
|
||
providers = provider_manager.list_provider_info()
|
||
```
|
||
|
||
## Plugin Packaging
|
||
|
||
Package your plugin as a ZIP file for distribution:
|
||
|
||
```bash
|
||
cd /path/to/plugins
|
||
zip -r my-plugin-1.0.0.zip my-plugin/
|
||
```
|
||
|
||
Users can install via URL:
|
||
|
||
```bash
|
||
qwenpaw plugin install https://example.com/my-plugin-1.0.0.zip
|
||
```
|
||
|
||
## FAQ
|
||
|
||
### Q: What QwenPaw APIs can plugins access?
|
||
|
||
A: Plugins access core functionality through `PluginApi`, including:
|
||
|
||
- Provider registration
|
||
- Middleware registration (`register_middleware`)
|
||
- Hook registration
|
||
- Custom command registration (`register_control_command`)
|
||
- HTTP router registration (`register_http_router`)
|
||
- Runtime helpers (provider_manager, etc.)
|
||
|
||
### Q: Can plugins modify QwenPaw's core behavior?
|
||
|
||
A: Yes, through `register_middleware` (inject AgentScope middlewares), `register_control_command`, `register_tool`, runtime hooks, and other PluginApi methods. Use with caution to avoid breaking core functionality.
|
||
|
||
### Q: Will plugins conflict with each other?
|
||
|
||
A: If multiple plugins register the same provider_id or command_name, the later one will override the earlier one. Use unique IDs.
|
||
|
||
## Example Plugins
|
||
|
||
### GPT Image 2 Tool Plugin
|
||
|
||
A tool plugin that adds OpenAI's GPT Image 2 image generation capability to QwenPaw agents.
|
||
|
||
**Requirements:**
|
||
|
||
- Minimum QwenPaw version: `1.1.5`
|
||
|
||
**Installation:**
|
||
|
||
```bash
|
||
# Clone the QwenPaw repository (if not already cloned)
|
||
git clone https://github.com/agentscope-ai/QwenPaw.git
|
||
cd QwenPaw
|
||
|
||
# Install the plugin
|
||
qwenpaw plugin install plugins/tool/gpt-image2
|
||
```
|
||
|
||
**Configuration:**
|
||
|
||
1. After installation, restart QwenPaw
|
||
2. Go to Agent Settings → Tools
|
||
3. Find "generate_image_gpt" tool
|
||
4. Click "Configure" and enter your OpenAI API Key
|
||
5. Enable the tool
|
||
|
||
**Usage:**
|
||
|
||
Once configured, agents can generate images by calling the tool:
|
||
|
||
```
|
||
User: Please generate an image of a cute cat playing in a garden
|
||
Agent: [Calls generate_image_gpt tool]
|
||
[Returns generated image]
|
||
```
|
||
|
||
**Features:**
|
||
|
||
- Supports multiple image sizes: 1024x1024, 1024x1792, 1792x1024
|
||
- Quality options: low, medium, high, auto
|
||
- Automatic API key validation
|
||
- Per-agent configuration (each agent can have its own API key)
|
||
|
||
For more details, see `plugins/tool/gpt-image2/README.md`.
|