From 939b9d34b9efe54fdfc53890790f91d56208e20c Mon Sep 17 00:00:00 2001 From: Yeuoly Date: Mon, 11 Nov 2024 18:12:50 +0800 Subject: [PATCH] enhance: onboarding --- cmd/commandline/init/category.go | 28 ++++- cmd/commandline/init/init.go | 28 ++--- cmd/commandline/init/language.go | 4 +- cmd/commandline/init/permission.go | 24 ++-- cmd/commandline/init/python.go | 15 ++- cmd/commandline/init/template.go | 8 ++ cmd/commandline/init/templates/README.md | 11 ++ .../init/templates/python/.env.example | 4 + .../init/templates/python/GUIDE.md | 112 ++++++++++++++++++ .../plugin_entities/plugin_declaration.go | 18 +++ 10 files changed, 222 insertions(+), 30 deletions(-) create mode 100644 cmd/commandline/init/template.go create mode 100644 cmd/commandline/init/templates/README.md create mode 100644 cmd/commandline/init/templates/python/.env.example create mode 100644 cmd/commandline/init/templates/python/GUIDE.md diff --git a/cmd/commandline/init/category.go b/cmd/commandline/init/category.go index dcc4b47..03b2cc6 100644 --- a/cmd/commandline/init/category.go +++ b/cmd/commandline/init/category.go @@ -6,6 +6,31 @@ import ( tea "github.com/charmbracelet/bubbletea" ) +const ( + // Colors + RESET = "\033[0m" + BOLD = "\033[1m" + + // Foreground colors + GREEN = "\033[32m" + YELLOW = "\033[33m" + BLUE = "\033[34m" +) + +const PLUGIN_GUIDE = `But before starting, you need some basic knowledge about the Plugin types, Plugin supports to extend the following abilities in Dify: +` + "\n" + BOLD + `- Tool` + RESET + `: ` + GREEN + `Tool Providers like Google Search, Stable Diffusion, etc. it can be used to perform a specific task.` + RESET + ` +` + BOLD + `- Model` + RESET + `: ` + GREEN + `Model Providers like OpenAI, Anthropic, etc. you can use their models to enhance the AI capabilities.` + RESET + ` +` + BOLD + `- Endpoint` + RESET + `: ` + GREEN + `Like Service API in Dify and Ingress in Kubernetes, you can extend a http service as an endpoint and control its logics using your own code.` + RESET + ` + +Based on the ability you want to extend, we have divided the Plugin into three types: ` + BOLD + `Tool` + RESET + `, ` + BOLD + `Model` + RESET + `, and ` + BOLD + `Extension` + RESET + `. + +` + BOLD + `- Tool` + RESET + `: ` + YELLOW + `It's a tool provider, but not only limited to tools, you can implement a endpoint there, for example, you need both ` + BLUE + `Sending Message` + RESET + YELLOW + ` and ` + BLUE + `Receiving Message` + RESET + YELLOW + ` if you are building a Discord Bot, ` + BOLD + `Tool` + RESET + YELLOW + ` and ` + BOLD + `Endpoint` + RESET + YELLOW + ` are both required.` + RESET + ` +` + BOLD + `- Model` + RESET + `: ` + YELLOW + `Just a model provider, extending others is not allowed.` + RESET + ` +` + BOLD + `- Extension` + RESET + `: ` + YELLOW + `Other times, you may only need a simple http service to extend the functionalities, ` + BOLD + `Extension` + RESET + YELLOW + ` is the right choice for you.` + RESET + ` +` + ` +What's more, we have provided the template for you, you can choose one of them below: +` + type category struct { cursor int } @@ -33,7 +58,8 @@ func (c category) Category() string { } func (c category) View() string { - s := "Select the type of plugin you want to create\n" + s := "Select the type of plugin you want to create, and press `Enter` to continue\n" + s += PLUGIN_GUIDE for i, category := range categories { if i == c.cursor { s += fmt.Sprintf("\033[32m-> %s\033[0m\n", category) diff --git a/cmd/commandline/init/init.go b/cmd/commandline/init/init.go index 8114d98..81d2bd5 100644 --- a/cmd/commandline/init/init.go +++ b/cmd/commandline/init/init.go @@ -191,28 +191,26 @@ func (m model) createPlugin() { } plugin_dir := filepath.Join(cwd, manifest.Name) - if err := os.MkdirAll(plugin_dir, 0o755); err != nil { - log.Error("failed to create plugin directory: %s", err) - return - } - manifest_file_path := filepath.Join(plugin_dir, "manifest.yaml") - if err := os.WriteFile(manifest_file_path, manifest_file, 0o644); err != nil { + if err := writeFile(filepath.Join(plugin_dir, "manifest.yaml"), string(manifest_file)); err != nil { log.Error("failed to write manifest file: %s", err) return } - // create _assets directory - assets_dir := filepath.Join(plugin_dir, "_assets") - if err := os.MkdirAll(assets_dir, 0o755); err != nil { - log.Error("failed to create assets directory: %s", err) + // create icon.svg + if err := writeFile(filepath.Join(plugin_dir, "_assets", "icon.svg"), string(icon)); err != nil { + log.Error("failed to write icon file: %s", err) return } - // create icon.svg - icon_file_path := filepath.Join(assets_dir, "icon.svg") - if err := os.WriteFile(icon_file_path, icon, 0o644); err != nil { - log.Error("failed to write icon file: %s", err) + // create README.md + readme, err := renderTemplate(README, manifest, []string{}) + if err != nil { + log.Error("failed to render README template: %s", err) + return + } + if err := writeFile(filepath.Join(plugin_dir, "README.md"), readme); err != nil { + log.Error("failed to write README file: %s", err) return } @@ -229,5 +227,5 @@ func (m model) createPlugin() { success = true - log.Info("plugin %s created successfully", manifest.Name) + log.Info("plugin %s created successfully, you can refer to `%s/GUIDE.md` for more information about how to develop it", manifest.Name, manifest.Name) } diff --git a/cmd/commandline/init/language.go b/cmd/commandline/init/language.go index da143f6..70231a1 100644 --- a/cmd/commandline/init/language.go +++ b/cmd/commandline/init/language.go @@ -28,7 +28,9 @@ func (l language) Language() constants.Language { } func (l language) View() string { - s := "Select the language you want to use for plugin development\n" + s := `Select the language you want to use for plugin development, and press ` + GREEN + `Enter` + RESET + ` to continue, +BTW, you need Python 3.10+ to develop the Plugin if you choose Python. +` for i, language := range languages { if i == l.cursor { s += fmt.Sprintf("\033[32m-> %s\033[0m\n", language) diff --git a/cmd/commandline/init/permission.go b/cmd/commandline/init/permission.go index 3b158d5..ae37eb1 100644 --- a/cmd/commandline/init/permission.go +++ b/cmd/commandline/init/permission.go @@ -62,29 +62,29 @@ func (p permission) View() string { s := "Configure the permissions of the plugin, use up and down to navigate, enter to select, after selection, press right to move to the next menu\n" s += "Backwards Invocation:\n" s += "Tools:\n" - s += fmt.Sprintf(" %sEnabled: %v\n", cursor("tool.enabled"), checked(p.permission.AllowInvokeTool())) + s += fmt.Sprintf(" %sEnabled: %v %s You can invoke tools inside Dify if it's enabled %s\n", cursor("tool.enabled"), checked(p.permission.AllowInvokeTool()), YELLOW, RESET) s += "Models:\n" - s += fmt.Sprintf(" %sEnabled: %v\n", cursor("model.enabled"), checked(p.permission.AllowInvokeModel())) - s += fmt.Sprintf(" %sLLM: %v\n", cursor("model.llm"), checked(p.permission.AllowInvokeLLM())) - s += fmt.Sprintf(" %sText Embedding: %v\n", cursor("model.text_embedding"), checked(p.permission.AllowInvokeTextEmbedding())) - s += fmt.Sprintf(" %sRerank: %v\n", cursor("model.rerank"), checked(p.permission.AllowInvokeRerank())) - s += fmt.Sprintf(" %sTTS: %v\n", cursor("model.tts"), checked(p.permission.AllowInvokeTTS())) - s += fmt.Sprintf(" %sSpeech2Text: %v\n", cursor("model.speech2text"), checked(p.permission.AllowInvokeSpeech2Text())) - s += fmt.Sprintf(" %sModeration: %v\n", cursor("model.moderation"), checked(p.permission.AllowInvokeModeration())) + s += fmt.Sprintf(" %sEnabled: %v %s You can invoke models inside Dify if it's enabled %s\n", cursor("model.enabled"), checked(p.permission.AllowInvokeModel()), YELLOW, RESET) + s += fmt.Sprintf(" %sLLM: %v %s You can invoke LLM models inside Dify if it's enabled %s\n", cursor("model.llm"), checked(p.permission.AllowInvokeLLM()), YELLOW, RESET) + s += fmt.Sprintf(" %sText Embedding: %v %s You can invoke text embedding models inside Dify if it's enabled %s\n", cursor("model.text_embedding"), checked(p.permission.AllowInvokeTextEmbedding()), YELLOW, RESET) + s += fmt.Sprintf(" %sRerank: %v %s You can invoke rerank models inside Dify if it's enabled %s\n", cursor("model.rerank"), checked(p.permission.AllowInvokeRerank()), YELLOW, RESET) + s += fmt.Sprintf(" %sTTS: %v %s You can invoke TTS models inside Dify if it's enabled %s\n", cursor("model.tts"), checked(p.permission.AllowInvokeTTS()), YELLOW, RESET) + s += fmt.Sprintf(" %sSpeech2Text: %v %s You can invoke speech2text models inside Dify if it's enabled %s\n", cursor("model.speech2text"), checked(p.permission.AllowInvokeSpeech2Text()), YELLOW, RESET) + s += fmt.Sprintf(" %sModeration: %v %s You can invoke moderation models inside Dify if it's enabled %s\n", cursor("model.moderation"), checked(p.permission.AllowInvokeModeration()), YELLOW, RESET) s += "Apps:\n" - s += fmt.Sprintf(" %sEnabled: %v\n", cursor("app.enabled"), checked(p.permission.AllowInvokeApp())) + s += fmt.Sprintf(" %sEnabled: %v %s Ability to invoke apps like BasicChat/ChatFlow/Agent/Workflow etc. %s\n", cursor("app.enabled"), checked(p.permission.AllowInvokeApp()), YELLOW, RESET) s += "Resources:\n" s += "Storage:\n" - s += fmt.Sprintf(" %sEnabled: %v\n", cursor("storage.enabled"), checked(p.permission.AllowInvokeStorage())) + s += fmt.Sprintf(" %sEnabled: %v %s Persistence storage for the plugin %s\n", cursor("storage.enabled"), checked(p.permission.AllowInvokeStorage()), YELLOW, RESET) if p.permission.AllowInvokeStorage() { s += fmt.Sprintf(" %sSize: %v\n", cursor("storage.size"), p.storageSizeEditor.View()) } else { - s += fmt.Sprintf(" %sSize: %v\n", cursor("storage.size"), "N/A") + s += fmt.Sprintf(" %sSize: %v %s The maximum size of the storage %s\n", cursor("storage.size"), "N/A", YELLOW, RESET) } s += "Endpoints:\n" - s += fmt.Sprintf(" %sEnabled: %v\n", cursor("endpoint.enabled"), checked(p.permission.AllowRegisterEndpoint())) + s += fmt.Sprintf(" %sEnabled: %v %s Ability to register endpoints %s\n", cursor("endpoint.enabled"), checked(p.permission.AllowRegisterEndpoint()), YELLOW, RESET) return s } diff --git a/cmd/commandline/init/python.go b/cmd/commandline/init/python.go index 19503e1..dced02b 100644 --- a/cmd/commandline/init/python.go +++ b/cmd/commandline/init/python.go @@ -82,6 +82,9 @@ var PYTHON_ENDPOINT_TEMPLATE []byte //go:embed templates/python/endpoint.yaml var PYTHON_ENDPOINT_MANIFEST_TEMPLATE []byte +//go:embed templates/python/GUIDE.md +var PYTHON_GUIDE []byte + func renderTemplate( original_template []byte, manifest *plugin_entities.PluginDeclaration, supported_model_types []string, ) (string, error) { @@ -98,6 +101,9 @@ func renderTemplate( "Author": manifest.Author, "PluginDescription": manifest.Description.EnUS, "SupportedModelTypes": supported_model_types, + "Version": manifest.Version, + "Date": manifest.CreatedAt, + "Category": manifest.Category(), }); err != nil { return "", err } @@ -115,7 +121,14 @@ func writeFile(path string, content string) error { func createPythonEnvironment( root string, entrypoint string, manifest *plugin_entities.PluginDeclaration, category string, ) error { - // TODO: enhance to use template renderer + guide, err := renderTemplate(PYTHON_GUIDE, manifest, []string{}) + if err != nil { + return err + } + + if err := writeFile(filepath.Join(root, "GUIDE.md"), guide); err != nil { + return err + } // create the python environment entrypoint_file_path := filepath.Join(root, fmt.Sprintf("%s.py", entrypoint)) diff --git a/cmd/commandline/init/template.go b/cmd/commandline/init/template.go new file mode 100644 index 0000000..e627f20 --- /dev/null +++ b/cmd/commandline/init/template.go @@ -0,0 +1,8 @@ +package init + +import ( + _ "embed" +) + +//go:embed templates/README.md +var README []byte diff --git a/cmd/commandline/init/templates/README.md b/cmd/commandline/init/templates/README.md new file mode 100644 index 0000000..288e641 --- /dev/null +++ b/cmd/commandline/init/templates/README.md @@ -0,0 +1,11 @@ +## {{ .PluginName }} + +**Author:** {{ .Author }} +**Version:** {{ .Version }} +**Date:** {{ .Date }} +**Type:** {{ .Category }} + +### Description + +{{ .Description }} + diff --git a/cmd/commandline/init/templates/python/.env.example b/cmd/commandline/init/templates/python/.env.example new file mode 100644 index 0000000..ef0f7ac --- /dev/null +++ b/cmd/commandline/init/templates/python/.env.example @@ -0,0 +1,4 @@ +INSTALL_METHOD=REMOTE +REMOTE_INSTALL_HOST=debug-plugin.dify.dev +REMOTE_INSTALL_PORT=5003 +REMOTE_INSTALL_KEY=********-****-****-****-************ diff --git a/cmd/commandline/init/templates/python/GUIDE.md b/cmd/commandline/init/templates/python/GUIDE.md new file mode 100644 index 0000000..57a5402 --- /dev/null +++ b/cmd/commandline/init/templates/python/GUIDE.md @@ -0,0 +1,112 @@ +## User Guide of how to develop a Dify Plugin + +Hi there, looks like you have already created a Plugin, now let's get you started with the development! + +### Choose a Plugin type you want to develop + +Before start, you need some basic knowledge about the Plugin types, Plugin supports to extend the following abilities in Dify: +- **Tool**: Tool Providers like Google Search, Stable Diffusion, etc. it can be used to perform a specific task. +- **Model**: Model Providers like OpenAI, Anthropic, etc. you can use their models to enhance the AI capabilities. +- **Endpoint**: Like Service API in Dify and Ingress in Kubernetes, you can extend a http service as an endpoint and control its logics using your own code. + +Based on the ability you want to extend, we have divided the Plugin into three types: **Tool**, **Model**, and **Extension**. + +- **Tool**: It's a tool provider, but not only limited to tools, you can implement a endpoint there, for example, you need both `Sending Message` and `Receiving Message` if you are building a Discord Bot, **Tool** and **Endpoint** are both required. +- **Model**: Just a model provider, extending others is not allowed. +- **Extension**: Other times, you may only need a simple http service to extend the functionalities, **Extension** is the right choice for you. + +I believe you have chosen the right type for your Plugin while creating it, if not, you can change it later by modifying the `manifest.yaml` file. + +### Manifest + +Now you can edit the `manifest.yaml` file to describe your Plugin, here is the basic structure of it: + +- version(version, required):Plugin's version +- type(type, required):Plugin's type, currently only supports `plugin`, future support `bundle` +- author(string, required):作者,在 Marketplace 中定义为组织名 +- label(label, required):Multi-language name +- created_at(RFC3339, required):Creation time, Marketplace requires that the creation time must be less than the current time +- icon(asset, required):Icon path +- resource (object):Resources to be applied + - memory (int64):Maximum memory usage, mainly related to resource application on SaaS for serverless, unit bytes + - permission(object):Permission application + - tool(object):Reverse call tool permission + - enabled (bool) + - model(object):Reverse call model permission + - enabled(bool) + - llm(bool) + - text_embedding(bool) + - rerank(bool) + - tts(bool) + - speech2text(bool) + - moderation(bool) + - node(object):Reverse call node permission + - enabled(bool) + - endpoint(object):Allow to register endpoint permission + - enabled(bool) + - app(object):Reverse call app permission + - enabled(bool) + - storage(object):Apply for persistent storage permission + - enabled(bool) + - size(int64):Maximum allowed persistent memory, unit bytes +- plugins(object, required):Plugin extension specific ability yaml file list, absolute path in the plugin package, if you need to extend the model, you need to define a file like openai.yaml, and fill in the path here, and the file on the path must exist, otherwise the packaging will fail. + - Format + - tools(list[string]): Extended tool suppliers, as for the detailed format, please refer to [Tool Guide](https://docs.dify.ai/docs/plugins/standard/tool_provider) + - models(list[string]):Extended model suppliers, as for the detailed format, please refer to [Model Guide](https://docs.dify.ai/docs/plugins/standard/model_provider) + - endpoints(list[string]):Extended Endpoints suppliers, as for the detailed format, please refer to [Endpoint Guide](https://docs.dify.ai/docs/plugins/standard/endpoint_group) + - Restrictions + - Not allowed to extend both tools and models + - Not allowed to have no extension + - Not allowed to extend both models and endpoints + - Currently only supports up to one supplier of each type of extension +- meta(object) + - version(version, required):manifest format version, initial version 0.0.1 + - arch(list[string], required):Supported architectures, currently only supports amd64 arm64 + - runner(object, required):Runtime configuration + - language(string):Currently only supports python + - version(string):Language version, currently only supports 3.12 + - entrypoint(string):Program entry, in python it should be main + +### Install Dependencies + +- First of all, you need a Python 3.10+ environment, as our SDK requires that. +- Then, install the dependencies: + ```bash + pip install -r requirements.txt + ``` +- If you want to add more dependencies, you can add them to the `requirements.txt` file, once you have set the runner to python in the `manifest.yaml` file, `requirements.txt` will be automatically generated and used for packaging and deployment. + +### Implement the Plugin + +Now you can start to implement your Plugin, by following these examples, you can quickly understand how to implement your own Plugin: + +- [OpenAI](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/openai): best practice for model provider +- [Google Search](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/google): a simple example for tool provider +- [Neko](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/neko): a funny example for endpoint group + +### Test and Debug the Plugin + +You may already noticed that a `.env.example` file in the root directory of your Plugin, just copy it to `.env` and fill in the corresponding values, there are some environment variables you need to set if you want to debug your Plugin locally. + +- `INSTALL_METHOD`: Set this to `remote`, your plugin will connect to a Dify instance through the network. +- `REMOTE_INSTALL_HOST`: The host of your Dify instance, you can use our SaaS instance `https://debug.dify.ai`, or self-hosted Dify instance. +- `REMOTE_INSTALL_PORT`: The port of your Dify instance, default is 5003 +- `REMOTE_INSTALL_KEY`: You should get your debugging key from the Dify instance you used, at the right top of the plugin management page, you can see a button with a `debug` icon, click it and you will get the key. + +Run the following command to start your Plugin: + +```bash +python -m main +``` + +Refresh the page of your Dify instance, you should be able to see your Plugin in the list now, but it will be marked as `debugging`, you can use it normally, but not recommended for production. + +### Package the Plugin + +After all, just package your Plugin by running the following command: + +```bash +dify-plugin plugin package ./ROOT_DIRECTORY_OF_YOUR_PLUGIN +``` + +you will get a `plugin.difypkg` file, that's all, you can submit it to the Marketplace now, look forward to your Plugin being listed! diff --git a/internal/types/entities/plugin_entities/plugin_declaration.go b/internal/types/entities/plugin_entities/plugin_declaration.go index 40d8bc3..57ed5e3 100644 --- a/internal/types/entities/plugin_entities/plugin_declaration.go +++ b/internal/types/entities/plugin_entities/plugin_declaration.go @@ -12,6 +12,14 @@ import ( "github.com/langgenius/dify-plugin-daemon/internal/utils/parser" ) +type PluginCategory string + +const ( + PLUGIN_CATEGORY_TOOL PluginCategory = "tool" + PLUGIN_CATEGORY_MODEL PluginCategory = "model" + PLUGIN_CATEGORY_EXTENSION PluginCategory = "extension" +) + type DifyManifestType string const ( @@ -224,6 +232,16 @@ type PluginDeclaration struct { Tool *ToolProviderDeclaration `json:"tool,omitempty" yaml:"tool,omitempty" validate:"omitempty"` } +func (p *PluginDeclaration) Category() PluginCategory { + if p.Tool != nil { + return PLUGIN_CATEGORY_TOOL + } + if p.Model != nil { + return PLUGIN_CATEGORY_MODEL + } + return PLUGIN_CATEGORY_EXTENSION +} + func (p *PluginDeclaration) UnmarshalJSON(data []byte) error { // First unmarshal the embedded struct if err := json.Unmarshal(data, &p.PluginDeclarationWithoutAdvancedFields); err != nil {