Model 6: Plugin Mode

Embed recipes into IDEs, CMS platforms, and chat applications

Embed recipes inside IDEs, CMS platforms, or chat apps with native host UX.

python
from praisonaiagents import Agent

agent = Agent(
    name="Plugin Host",
    instructions="Describe plugin-mode recipe integration for editor extensions.",
)
agent.start("Expose a recipe as a VS Code command.")

The user triggers the plugin; the host forwards context to a local or remote recipe runner.

Diagram

Plugin Mode

📋 Host Action

🔌 Plugin Adapter

✅ In-app Result

How It Works

Diagram
Recipe RunnerPluginUserRecipe RunnerPluginUserTrigger command in host appPOST recipe + host contextOutputRender result in the UI
Diagram

Recipe Runner

Host Platform

HTTP

HTTP

IDE / CMS / Chat

Plugin Adapter

Local Sidecar

Remote Runner

Recipe Engine

Plugin Mode wraps recipes in a platform-specific adapter that translates between the host platform’s API and PraisonAI’s HTTP interface.

Pros & Cons

Pros

  • Native UX – Feels like part of the host platform
  • Context-aware – Access to platform context (files, selection, etc.)
  • User-friendly – No CLI or API knowledge needed
  • Discoverable – Users find recipes in familiar UI
  • Streaming support – Real-time feedback in UI

Cons

  • Platform-specific – Each platform needs its own adapter
  • Maintenance burden – Keep up with platform API changes
  • Distribution – Plugin store approval processes
  • Limited control – Platform sandboxing restrictions

Step-by-Step Tutorial

  1. Choose Your Platform
    PlatformExtension TypeCommunication
    VS CodeExtensionHTTP to sidecar
    JetBrainsPluginHTTP to sidecar
    ObsidianPluginHTTP to sidecar
    SlackAppHTTP to remote
    DiscordBotHTTP to remote
    WordPressPluginHTTP to remote
  2. Start Recipe Runner
    bash
    # Local development
    praisonai serve recipe --port 8765
    
    # Or use remote runner
    export PRAISONAI_ENDPOINTS_URL=https://api.example.com
  3. Create Plugin Manifest
    json
    {
      "name": "praisonai-recipes",
      "displayName": "PraisonAI Recipes",
      "version": "1.0.0",
      "description": "Run AI recipes from your editor",
      "engines": {
        "vscode": "^1.80.0"
      },
      "activationEvents": [
        "onCommand:praisonai.runRecipe"
      ],
      "contributes": {
        "commands": [
          {
            "command": "praisonai.runRecipe",
            "title": "Run Recipe"
          }
        ],
        "configuration": {
          "title": "PraisonAI",
          "properties": {
            "praisonai.serverUrl": {
              "type": "string",
              "default": "http://localhost:8765",
              "description": "PraisonAI server URL"
            },
            "praisonai.apiKey": {
              "type": "string",
              "description": "API key for authentication"
            }
          }
        }
      }
    }
  4. Implement Plugin Logic

    See platform-specific examples below.

VS Code Extension Example

typescript
// extension.ts
import * as vscode from 'vscode';

interface RecipeResult {
  ok: boolean;
  output: string;
  error?: string;
}

class PraisonAIClient {
  private baseUrl: string;
  private apiKey: string | undefined;

  constructor() {
    const config = vscode.workspace.getConfiguration('praisonai');
    this.baseUrl = config.get('serverUrl', 'http://localhost:8765');
    this.apiKey = config.get('apiKey');
  }

  async listRecipes(): Promise<any[]> {
    const response = await fetch(`${this.baseUrl}/v1/recipes`, {
      headers: this.getHeaders(),
    });
    const data = await response.json();
    return data.recipes || [];
  }

  async runRecipe(name: string, input: Record<string, any>): Promise<RecipeResult> {
    const response = await fetch(`${this.baseUrl}/v1/recipes/run`, {
      method: 'POST',
      headers: this.getHeaders(),
      body: JSON.stringify({ recipe: name, input }),
    });
    return response.json();
  }

  private getHeaders(): Record<string, string> {
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
    };
    if (this.apiKey) {
      headers['X-API-Key'] = this.apiKey;
    }
    return headers;
  }
}

export function activate(context: vscode.ExtensionContext) {
  const client = new PraisonAIClient();

  // Command: Run Recipe
  const runRecipeCmd = vscode.commands.registerCommand(
    'praisonai.runRecipe',
    async () => {
      // Get available recipes
      const recipes = await client.listRecipes();

      // Show quick pick
      const selected = await vscode.window.showQuickPick(
        recipes.map(r => ({
          label: r.name,
          description: r.description,
        })),
        { placeHolder: 'Select a recipe to run' }
      );

      if (!selected) return;

      // Get current selection as input
      const editor = vscode.window.activeTextEditor;
      const selection = editor?.document.getText(editor.selection);

      // Run with progress
      await vscode.window.withProgress(
        {
          location: vscode.ProgressLocation.Notification,
          title: `Running ${selected.label}...`,
          cancellable: false,
        },
        async () => {
          const result = await client.runRecipe(selected.label, {
            text: selection || '',
            filename: editor?.document.fileName,
          });

          if (result.ok) {
            // Show result in new document
            const doc = await vscode.workspace.openTextDocument({
              content: result.output,
              language: 'markdown',
            });
            await vscode.window.showTextDocument(doc);
          } else {
            vscode.window.showErrorMessage(`Recipe failed: ${result.error}`);
          }
        }
      );
    }
  );

  context.subscriptions.push(runRecipeCmd);
}

export function deactivate() {}

Obsidian Plugin Example

typescript
// main.ts
import { App, Plugin, PluginSettingTab, Setting, Notice } from 'obsidian';

interface PraisonAISettings {
  serverUrl: string;
  apiKey: string;
}

const DEFAULT_SETTINGS: PraisonAISettings = {
  serverUrl: 'http://localhost:8765',
  apiKey: '',
};

export default class PraisonAIPlugin extends Plugin {
  settings: PraisonAISettings;

  async onload() {
    await this.loadSettings();

    // Add command
    this.addCommand({
      id: 'run-recipe',
      name: 'Run Recipe',
      editorCallback: async (editor) => {
        const selection = editor.getSelection();

        // Fetch recipes
        const recipes = await this.listRecipes();

        // Show modal to select recipe
        // (simplified - use a proper modal in production)
        const recipeName = recipes[0]?.name;

        if (recipeName) {
          new Notice(`Running ${recipeName}...`);

          const result = await this.runRecipe(recipeName, {
            text: selection,
          });

          if (result.ok) {
            editor.replaceSelection(result.output);
            new Notice('Recipe completed!');
          } else {
            new Notice(`Error: ${result.error}`);
          }
        }
      },
    });

    this.addSettingTab(new PraisonAISettingTab(this.app, this));
  }

  async listRecipes(): Promise<any[]> {
    const response = await fetch(`${this.settings.serverUrl}/v1/recipes`, {
      headers: this.getHeaders(),
    });
    const data = await response.json();
    return data.recipes || [];
  }

  async runRecipe(name: string, input: Record<string, any>): Promise<any> {
    const response = await fetch(`${this.settings.serverUrl}/v1/recipes/run`, {
      method: 'POST',
      headers: this.getHeaders(),
      body: JSON.stringify({ recipe: name, input }),
    });
    return response.json();
  }

  private getHeaders(): Record<string, string> {
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
    };
    if (this.settings.apiKey) {
      headers['X-API-Key'] = this.settings.apiKey;
    }
    return headers;
  }

  async loadSettings() {
    this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
  }

  async saveSettings() {
    await this.saveData(this.settings);
  }
}

class PraisonAISettingTab extends PluginSettingTab {
  plugin: PraisonAIPlugin;

  constructor(app: App, plugin: PraisonAIPlugin) {
    super(app, plugin);
    this.plugin = plugin;
  }

  display(): void {
    const { containerEl } = this;
    containerEl.empty();

    new Setting(containerEl)
      .setName('Server URL')
      .setDesc('PraisonAI server URL')
      .addText((text) =>
        text
          .setPlaceholder('http://localhost:8765')
          .setValue(this.plugin.settings.serverUrl)
          .onChange(async (value) => {
            this.plugin.settings.serverUrl = value;
            await this.plugin.saveSettings();
          })
      );

    new Setting(containerEl)
      .setName('API Key')
      .setDesc('API key for authentication')
      .addText((text) =>
        text
          .setPlaceholder('your-api-key')
          .setValue(this.plugin.settings.apiKey)
          .onChange(async (value) => {
            this.plugin.settings.apiKey = value;
            await this.plugin.saveSettings();
          })
      );
  }
}

Slack App Example

python
# slack_bot.py
import os
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
import requests

app = App(token=os.environ["SLACK_BOT_TOKEN"])

PRAISONAI_URL = os.environ.get("PRAISONAI_ENDPOINTS_URL", "http://localhost:8765")
PRAISONAI_KEY = os.environ.get("PRAISONAI_ENDPOINTS_API_KEY")

def run(recipe_name: str, input_data: dict) -> dict:
    """Call PraisonAI recipe runner."""
    headers = {"Content-Type": "application/json"}
    if PRAISONAI_KEY:
        headers["X-API-Key"] = PRAISONAI_KEY

    response = requests.post(
        f"{PRAISONAI_URL}/v1/recipes/run",
        headers=headers,
        json={"recipe": recipe_name, "input": input_data},
        timeout=60
    )
    return response.json()


@app.command("/recipe")
def handle_recipe_command(ack, say, command):
    """Handle /recipe slash command."""
    ack()

    text = command.get("text", "").strip()
    parts = text.split(" ", 1)
    recipe_name = parts[0] if parts else ""
    input_text = parts[1] if len(parts) > 1 else ""

    if not recipe_name:
        say("Usage: /recipe <recipe-name> [input text]")
        return

    say(f"Running recipe `{recipe_name}`...")

    result = run(recipe_name, {"text": input_text, "user": command["user_name"]})

    if result.get("ok"):
        say(f"✅ Result:\n```\n{result.get('output', '')}\n```")
    else:
        say(f"❌ Error: {result.get('error', 'Unknown error')}")


@app.event("app_mention")
def handle_mention(event, say):
    """Handle @mentions."""
    text = event.get("text", "")
    # Remove bot mention
    text = text.split(">", 1)[-1].strip()

    # Default to a general-purpose recipe
    result = run("chat-assistant", {"message": text})

    if result.get("ok"):
        say(result.get("output", ""))
    else:
        say(f"Sorry, I encountered an error: {result.get('error')}")


if __name__ == "__main__":
    handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
    handler.start()

Troubleshooting

Plugin can't connect to sidecar

Ensure the sidecar is running and accessible:

bash
# Check if server is running
curl http://localhost:8765/health

# Check firewall/network
# Some platforms sandbox network access
CORS errors in browser-based plugins

Configure CORS on the sidecar:

bash
export PRAISONAI_CORS_ORIGINS="*"
praisonai serve recipe
Authentication issues

Verify API key is configured correctly:

typescript
// Check settings
console.log('API Key configured:', !!settings.apiKey);

// Test with curl
// curl -H "X-API-Key: your-key" http://localhost:8765/health
Slow response times

Consider using streaming for better UX:

typescript
// Use SSE for streaming
const response = await fetch(`${baseUrl}/v1/recipes/stream`, {
  method: 'POST',
  headers: { 'Accept': 'text/event-stream', ...headers },
  body: JSON.stringify({ recipe: name, input }),
});

const reader = response.body.getReader();
// Process stream...

Security & Ops Notes

  • API key storage – Use platform’s secure storage (VS Code SecretStorage, etc.)
  • Input sanitization – Validate user input before sending to recipes
  • Network security – Use HTTPS for remote runners
  • Permissions – Request minimal permissions in plugin manifest
  • Rate limiting – Implement client-side rate limiting
typescript
// VS Code secure storage example
const secretStorage = context.secrets;
await secretStorage.store('praisonai-api-key', apiKey);
const storedKey = await secretStorage.get('praisonai-api-key');

Plugin Distribution

PlatformDistribution Channel
VS CodeVS Code Marketplace
JetBrainsJetBrains Marketplace
ObsidianCommunity Plugins
SlackSlack App Directory
DiscordDiscord App Directory

Best Practices

Store credentials in the host's secure storage

Use VS Code SecretStorage or the equivalent so API keys never sit in plaintext settings or source.

Send only the context the recipe needs

Forward the selection or active file rather than the whole workspace to keep requests small and reduce leakage.

Stream results for responsive UX

Wire the plugin to /v1/recipes/stream so users see incremental output inside the host UI instead of a frozen progress bar.

One adapter per platform, one runner shared

Keep platform-specific glue thin and point every adapter at the same sidecar or remote runner to avoid duplicating recipe logic.


Related