Создание собственного инструмента Optimizely Opal с помощью OCP SDK

Недавно я работал над некоторыми специальными инструментами Opal, и при поиске вариантов хостинга было несложно использовать платформу Optimizely OCP, хотя документация великолепна, на самом деле нет никаких реальных статей о разработке инструментов для размещения на OCP или справочных статей. В большинстве статей не рассматривается использование OCP в качестве платформы доставки.

После публикации моего первого стороннего инструмента Opal, размещенного на OCP, я решил поделиться полным обзором.

Здесь мы узнаем, как создать готовый к использованию инструмент Opal, который интегрирует сторонние API с платформой Connect Platform (OCP) Optimizely. В качестве реального примера я буду использовать ActiveCampaign, инструмент автоматизации маркетинга и CRM.

Введение

Помощник Opal AI от Optimizely становится значительно более мощным, когда он может взаимодействовать со сторонними инструментами, такими как ваш маркетинговый пакет. В этой статье мы рассмотрим создание инструмента Opal, интегрирующего REST API, развернутого на платформе Connect Platform (OCP) Optimizely.

В конце вы поймете:

  • Как структурировать инструмент Opal, размещенный на OCP
  • Шаблоны аутентификации для учетных данных, предоставленных пользователем.
  • Сборка с использованием TypeScript и OCP SDK
  • Развертывание и тестирование вашего инструмента в производстве

Что мы строим

Мы создадим Инструмент ActiveCampaign Opal например, через конечную точку управления контактами.

Это позволяет Opal выполнять такие действия, как «Создать контакт в ActiveCampaign для [email protected]» или «Показать все выгодные предложения».

Предварительные условия

Прежде чем начать, убедитесь, что у вас есть:

  • Установлен Node.js 18+
  • Аккаунт Optimizely OCP
  • Базовые знания TypeScript
  • Менеджер пакетов Yarn (npm install -g Yarn)

Настройка проекта

1. Инициализируйте свой проект

mkdir ActiveCampaignOpal
cd ActiveCampaignOpal
npm init -y

2. Установите основные зависимости

yarn add @optimizely-opal/[email protected]
yarn add @zaiusinc/app-sdk@^2.3.0
yarn add @zaiusinc/node-sdk@^2.0.0
yarn add axios

3. Установите зависимости разработки

yarn add -D typescript @types/node
yarn add -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
yarn add -D copyfiles rimraf

4. Критично: добавьте разрешения зависимостей

Это важно для успешных сборок OCP. Добавьте в свой package.json:

{
  "resolutions": {
    "grpc-boom": "^3.0.11"
  }
}

Почему? Процесс сборки Docker OCP требует этого разрешения, чтобы предотвратить сбои сборки с внутренними зависимостями gRPC.

Структура проекта

Организуйте свой проект, следуя следующей структуре:

ActiveCampaignOpal/
├── src/
│   ├── index.ts                      # Entry point
│   ├── tools.ts                      # Tool functions with @tool decorators
│   ├── activecampaign-client.ts      # API client wrapper
│   ├── types.ts                      # TypeScript type definitions
│   └── lifecycle/
│       └── Lifecycle.ts              # App lifecycle handlers
├── forms/
│   └── settings.yml                  # User credential input form
├── assets/
│   ├── icon.svg                      # 64x64 app icon
│   ├── logo.svg                      # 200x200 app logo
│   └── directory/
│       └── overview.md               # App marketplace description
├── app.yml                           # OCP app configuration
├── package.json
├── tsconfig.json
└── yarn.lock

Основные файлы конфигурации

Конфигурация TypeScript (tsconfig.json)

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "declaration": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Ключевые настройки:

  • ExperimentDecorators: true — требуется для декораторов @tool.
  • emiteDecoratorMetadata: true — включает метаданные декоратора.

Пакетные сценарии

Добавьте эти скрипты в package.json:

{
  "scripts": {
    "build": "yarn && npx rimraf dist && npx tsc && copyfiles app.yml dist && copyfiles --up 1 "src/**/*.{yml,yaml}" dist",
    "lint": "eslint src --ext .ts",
    "test": "exit 0"
  }
}

Важный: Процесс сборки OCP запускается yarn lint && yarn build && yarn testпоэтому все три сценария должны существовать.

Конфигурация приложения OCP (app.yml)

meta:
  app_id: your_opal_tool
  contact_email: [email protected]
  support_url: https://github.com/yourusername/yourrepo/issues
  categories:
    - Opal
  availability:
    - all
runtime: node22
functions:
  opal_tool:
    opal_tool: true
    entry_point: ActiveCampaignToolFunction
    description: ActiveCampaign Opal tool function

Ключевые моменты:

  • opal_tool: true — включает автоматическое соединение учетных данных.
  • enter_point — должно соответствовать имени вашего экспортированного класса.
  • среда выполнения: node22 — используйте последнюю стабильную среду выполнения Node.js.

Создание API-клиента

Создайте чистую обертку вокруг стороннего API:

// src/activecampaign-client.ts
import axios, { AxiosInstance } from 'axios';
import { ActiveCampaignCredentials, Contact, Deal, Tag } from './types';

export class ActiveCampaignClient {
  private client: AxiosInstance;
  constructor(credentials: ActiveCampaignCredentials) {
    this.client = axios.create({
      baseURL: credentials.apiUrl,
      headers: {
        'Api-Token': credentials.apiKey,
        'Content-Type': 'application/json'
      }
    });
  }

  async createContact(data: {
    email: string;
    firstName?: string;
    lastName?: string;
    phone?: string;
  }): Promise {
    const response = await this.client.post('/api/3/contacts', {
      contact: {
        email: data.email,
        firstName: data.firstName,
        lastName: data.lastName,
        phone: data.phone
      }
    });
    return response.data.contact;
  }
  // Additional methods for deals, tags, lists, etc.
}

Лучшие практики:

  • Проверка учетных данных в конструкторе
  • Используйте типизированные ответы
  • Постоянно обрабатывайте ошибки
  • Сохраняйте логику API отдельно от логики инструмента Opal.
Read more:  Венесуэла, две итальянцы Америко де Грация и Маргарита Ассенц освобождены. Трентини остается в клетке

Создание функций инструмента

Вот где происходит волшебство. Используйте декоратор @tool, чтобы предоставить функции Opal:

// src/tools.ts

import {
    ToolFunction,
    tool,
    ParameterType,
    OptiIdAuthData
} from '@optimizely-opal/opal-tool-ocp-sdk';
import {
    ActiveCampaignClient
} from './activecampaign-client';

export class ActiveCampaignToolFunction extends ToolFunction {
    /**
     * Extract credentials from OCP's auth system
     */

    private getCredentials(authData ? : OptiIdAuthData): ActiveCampaignCredentials {

        if (!authData?.credentials) {
            throw new Error('ActiveCampaign credentials not provided');
        }
        const credentials = authData.credentials as any;
        return {
            apiUrl: credentials.api_url || credentials.apiUrl,
            apiKey: credentials.api_key || credentials.apiKey
        };
    }

    private getClient(authData ? : OptiIdAuthData): ActiveCampaignClient {
        const credentials = this.getCredentials(authData);
        return new ActiveCampaignClient(credentials);
    }

    @tool({
        name: 'activecampaign_create_contact',
        description: 'Create a new contact in ActiveCampaign',
        endpoint: '/create-contact',
        parameters: [{
                name: 'email',
                type: ParameterType.String,
                description: 'Contact email address',
                required: true
            },
            {
                name: 'firstName',
                type: ParameterType.String,
                description: 'Contact first name',
                required: false
            }
        ],
        authRequirements: [{
            provider: 'OptiID',
            scopeBundle: 'activecampaign',
            required: true
        }]
    })
    async createContact(
        params: {
            email: string;firstName ? : string
        },
        authData ? : OptiIdAuthData
    ) {
        try {
            const client = this.getClient(authData);
            const contact = await client.createContact(params);

            return {
                success: true,
                message: `Contact created with ID: ${contact.id}`,
                contact: {
                    id: contact.id,
                    email: contact.email,
                    firstName: contact.firstName
                }
            };
        } catch (error: any) {
            return {
                success: false,
                error: error.message || 'Failed to create contact'
            };
        }
    }
}
// src/tools.ts
import {
    ToolFunction,
    tool,
    ParameterType,
    OptiIdAuthData
} from '@optimizely-opal/opal-tool-ocp-sdk';
import {
    ActiveCampaignClient
} from './activecampaign-client';
export class ActiveCampaignToolFunction extends ToolFunction {
    /**

   * Extract credentials from OCP's auth system

   */
    private getCredentials(authData ? : OptiIdAuthData): ActiveCampaignCredentials {
        if (!authData?.credentials) {
            throw new Error('ActiveCampaign credentials not provided');
        }
        const credentials = authData.credentials as any;
        return {
            apiUrl: credentials.api_url || credentials.apiUrl,
            apiKey: credentials.api_key || credentials.apiKey
        };
    }
    private getClient(authData ? : OptiIdAuthData): ActiveCampaignClient {
        const credentials = this.getCredentials(authData);
        return new ActiveCampaignClient(credentials);
    }
    @tool({
        name: 'activecampaign_create_contact',
        description: 'Create a new contact in ActiveCampaign',
        endpoint: '/create-contact',
        parameters: [{
                name: 'email',
                type: ParameterType.String,
                description: 'Contact email address',
                required: true
            },
            {
                name: 'firstName',
                type: ParameterType.String,
                description: 'Contact first name',
                required: false
            }
        ],
        authRequirements: [{
            provider: 'OptiID',
            scopeBundle: 'activecampaign',
            required: true
        }]
    })
    async createContact(
        params: {
            email: string;firstName ? : string
        },
        authData ? : OptiIdAuthData
    ) {
        try {
            const client = this.getClient(authData);
            const contact = await client.createContact(params);
            return {
                success: true,
                message: `Contact created with ID: ${contact.id}`,
                contact: {
                    id: contact.id,
                    email: contact.email,
                    firstName: contact.firstName
                }
            };
        } catch (error: any) {
            return {
                success: false,
                error: error.message || 'Failed to create contact'
            };
        }
    }
}

Понимание декоратора @tool

Конфигурация декоратора сообщает OCP, как представить вашу функцию:

  • имя – Уникальный идентификатор, который Opal использует для вызова этой функции.
  • описание – Текст справки, отображаемый пользователям и ИИ.
  • конечная точка – Путь к конечной точке HTTP (относительно базового URL-адреса вашей функции)
  • параметры – Массив типизированных параметров с описаниями
  • требования к авторизации – Сообщает OCP, что для этой функции необходимы учетные данные.

Процесс аутентификации

Вот что происходит, когда пользователь настраивает и использует ваш инструмент:

  1. Пользователь заполняет форму настроек → api_url и api_key сохранены в Storage.settings
  2. OCP читает настройки → Автоматически преобразуется в формат учетных данных OptiID.
  3. Пользователь вызывает инструмент через Opal → OCP вводит параметр authData
  4. Ваш инструмент извлекает учетные данные → getCredentials(authData) извлекает их.
  5. Вызов API выполнен → Использование учетных данных пользователя

Этот шаблон специфично для типа функции opal_tool OCP и автоматически управляет управлением учетными данными.

Создание формы настроек

Разрешить пользователям настраивать свои учетные данные API:

# forms/settings.yml
sections:
  - id: activecampaign_auth
    title: ActiveCampaign Configuration
    description: Configure your ActiveCampaign API credentials
    fields:
      - name: api_url
        type: text
        label: ActiveCampaign API URL
        placeholder: https://youraccountname.api-us1.com
        required: true
        help_text: Your ActiveCampaign API base URL (found in Settings → Developer)
      - name: api_key
        type: text
        label: ActiveCampaign API Key
        placeholder: Enter your API key
        required: true
        secret: true
        help_text: Your API key (found in Settings → Developer)

Ключевые особенности:

  • secret: true — маскирует ввод ключа API.
  • help_text — указывает пользователям, где найти учетные данные.
  • требуется: true — предотвращает сохранение неполной конфигурации.

Реализация обработчиков жизненного цикла

Управляйте установкой, обновлением и изменением настроек приложения:

// src/lifecycle/Lifecycle.ts
import { AbstractLifecycle, storage } from '@zaiusinc/app-sdk';

export default class Lifecycle extends AbstractLifecycle {
  
  async onInstall(event: any) {
    console.log('ActiveCampaign Opal Tool installed');
    return { success: true };
  }

  async onSettingsForm(section: string, formData: any) {
    if (section === 'activecampaign_auth') {
      // Validate credentials before saving
      const { api_url, api_key } = formData;
      
      if (!api_url || !api_key) {
        throw new Error('Both API URL and API Key are required');
      }

      // Save to storage
      await storage.settings.put(section, formData);
      
      return {
        success: true,
        message: 'ActiveCampaign credentials saved successfully!'
      };
    }
    
    return { success: true };
  }

  async onUpgrade(event: any, previousVersion: string) {
    console.log(`Upgrading from ${previousVersion}`);
    return { success: true };
  }
}

Почему жизненный цикл имеет значение:

  • onInstall – Инициализировать настройки по умолчанию, создавать ресурсы
  • onSettingsForm – Проверка и сохранение конфигурации пользователя.
  • onUpgrade – Миграция данных между версиями
Read more:  PSNI подтверждает, что в результате столкновения с А5 погиб мужчина - Highland Radio

Точка входа и экспорт

Соединяем все вместе:

// src/index.ts
export { ActiveCampaignToolFunction } from './tools.js';
export { default as Lifecycle } from './lifecycle/Lifecycle.js';

Критическое примечание: Используйте расширения .js при импорте, даже если вы пишете TypeScript. Это помогает улучшить разрешение модулей в скомпилированном выводе.

Сборка и тестирование локально

Создайте свой инструмент

yarn build

При этом TypeScript компилируется, копируется app.yml и подготавливается папка dist/ для развертывания.

Проверьте свою конфигурацию

# Install OCP CLI globally
npm install -g @optimizely/ocp-cli

# Login to OCP
ocp login

# Prepare for deployment (validates and bumps version)
ocp app prepare --bump-dev-version

Команда подготовки:

  • Проверяет структуру app.yml
  • Проверяет наличие всех необходимых файлов
  • Увеличивает номер версии разработки.
  • Создает образ Docker с вашим кодом.
  • Запускает Yarn lint && Yarn build && тест пряжи в контейнере.

Развертывание в OCP

После прохождения проверки ваш инструмент будет автоматически развернут:

приложение ocp подготовить –bump-dev-version

Вывод покажет:

Building Docker image...
Running yarn install...
Running yarn lint...
Running yarn build...
Running yarn test...
Pushing to registry...
Deployed [email protected]

Установка в вашу учетную запись

# Get your public API key from OCP dashboard
ocp directory install [email protected] YOUR_PUBLIC_API_KEY

Настройка в Opal

  1. Перейдите к настройкам приложения OCP.
  2. Заполните форму настроек с вашими учетными данными ActiveCampaign
  3. Скопируйте URL-адрес обнаружения показано в деталях приложения
  4. В Оптимизели Опалперейдите в «Настройки» → «Инструменты».
  5. Добавить инструмент → Пользовательский инструмент → Вставьте URL-адрес обнаружения
  6. Тестирование инструмента: попробуйте «Составить список моих контактов ActiveCampaign».

Распространенные ошибки и решения

1. Сборка завершается с ошибкой %5E3.0.11.

Проблема: Отсутствующие разрешения в package.json

Решение:

{
  "resolutions": {
    "grpc-boom": "^3.0.11"
  }
}

2. Аутентификация не работает

Проблема: Неправильное использование шаблона аутентификации OptiID

Решение: Гарантировать:

  • Форма настроек сохраняется в хранилище.settings.
  • Инструменты имеют authRequirements в декораторе @tool.
  • app.yml имеет opal_tool: true
  • Вы читаете из authData.credentials

3. Инструменты, не отображающиеся в Discovery

Проблема: Класс не экспортирован или несовпадение точки входа.

Решение:

  • Убедитесь, что app.yml enter_point точно соответствует имени вашего класса.
  • Убедитесь, что src/index.ts экспортирует класс: экспорт { YourToolFunction }
  • Перестройка: сборка пряжи

4. Ошибки декоратора

Проблема: Конфигурация TypeScript не поддерживает декораторы

Решение: Включите в tsconfig.json:

{
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true
}

Расширенные шаблоны

Четко определенные описания инструментов

Одним из советов является рассмотрение более описательных определений инструментов. Мы обнаружили, что Opal (и LLM в целом) лучше работают с инструментами, которые описаны более подробно. Например, когда использовать инструмент, а когда не использовать его, примеры вызовов, крайние случаи и т. д.

В качестве примера приведем один из внутренних инструментов Optimizely в OCP:

@tool({

    name: 'run_report',
    description: `
      Runs a Google Analytics Data API report.
      The Google Analytics Data API lets you query event-based analytics data from GA4 properties.
      Reports can include user demographics, traffic sources, engagement events, conversions,
      e-commerce performance, and time-based trends.
      It accepts customize queries with dimensions (e.g., country, device, page)
      and metrics (e.g., active users, sessions, conversions) to analyze how users
      interact with some app or website.

      ⚠️ CRITICAL WARNING: NEVER include "dateRange" in the dimensions array.

      This is ALWAYS invalid and will cause errors.
      JSON Parameter Example:

      ```json
      {
        "dateRanges": [
          {
            "startDate": "2025-08-01",
            "endDate": "2025-08-31",
            "name": "August"
          },
          {
            "startDate": "2025-07-01",
            "endDate": "2025-07-31",
            "name": "July"
          }
        ],
        "dimensions": ["country", "deviceCategory", "sessionSource"],
        "metrics": ["activeUsers", "sessions", "bounceRate", "averageSessionDuration"],
        "dimensionFilter": {
          "andGroup": {
            "expressions": [
              {
                "filter": {
                  "fieldName": "country",
                  "stringFilter": {
                    "value": "United States",
                    "matchType": "EXACT",
                    "caseSensitive": false
                  }
                }
              },
              {
                "filter": {
                  "fieldName": "deviceCategory",
                  "inListFilter": {
                    "values": ["desktop", "mobile"],
                    "caseSensitive": false
                  }
                }
              }
            ]
          }
        },
        "metricFilter": {
          "filter": {
            "fieldName": "sessions",
            "numericFilter": {
              "operation": "GREATER_THAN",
              "value": {
                "int64Value": "100"
              }
            }
          }
        },
        "orderBys": [
          {
            "desc": true,
            "metric": {
              "metricName": "sessions"
            }
          }
        ],
        "limit": 1000,
        "offset": 0,
        "currencyCode": "USD",
        "returnPropertyQuota": true
      }
      ```

      ❌ FORBIDDEN: "dateRange" is NOT a valid dimension. NEVER add it to the dimensions array.
      ✅ CORRECT: The example above shows valid dimensions: country, deviceCategory, sessionSource

      When using multiple date ranges, the API automatically includes date range information in the response.
      Use the "name" field in each dateRange object to distinguish between periods.
    `,
    endpoint: '/tools/run_report',
    parameters: [
      {
        name: 'dateRanges',
        type: ParameterType.String,
        description: `
          A list of date ranges
          (https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/DateRange)
          to include in the report.
          Each item is a JSON object with "startDate", "endDate", and optionally "name" fields.

          ### Important Constraints:

          - Maximum of 4 date ranges allowed
          - If date ranges are consecutive/connected (e.g., Oct 1-31, Nov 1-30, Dec 1-31),
            combine them into a single range (e.g., Oct 1-Dec 31) instead of separate ranges
          - Only use multiple date ranges when comparing non-consecutive periods

          ### Hints for "dateRanges":
          ${getDateRangesHints()}
        `,
        required: true,
      },

Расширенное форматирование ответа

Read more:  Ужесточение ограничения на загрязнение окружающей среды — доступная экономическая победа для Калифорнии

Возвращает структурированные данные, которые Opal может четко представить:

async getDeal(params: { dealId: string }, authData?: OptiIdAuthData) {
  const client = this.getClient(authData);
  const deal = await client.getDeal(params.dealId);
  
  return {
    success: true,
    deal: {
      id: deal.id,
      title: deal.title,
      value: `$${(deal.value / 100).toFixed(2)}`, // Format currency
      status: deal.status,
      contact: deal.contact ? {
        id: deal.contact.id,
        name: `${deal.contact.firstName} ${deal.contact.lastName}`,
        email: deal.contact.email
      } : null,
      url: `https://youraccountname.activehosted.com/app/deals/${deal.id}`
    }
  };
}

Кэширование для повышения производительности

private contactCache = new Map();
private CACHE_TTL = 5 * 60 * 1000; // 5 minutes

async getContact(params: { contactId: string }, authData?: OptiIdAuthData) {
  const cached = this.contactCache.get(params.contactId);
  if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
    return { success: true, contact: cached.data, cached: true };
  }

  const client = this.getClient(authData);
  const contact = await client.getContact(params.contactId);
  
  this.contactCache.set(params.contactId, { data: contact, timestamp: Date.now() });
  return { success: true, contact, cached: false };
}

Производственные аспекты

Стратегия управления версиями

Используйте семантическое управление версиями в app.yml:

  • Разработка: 1.0.0-dev.X (автоматически добавляется с помощью –bump-dev-version)
  • Постановка: 1.0.0-rc.1 (кандидат на выпуск)
  • Производство: 1.0.0 (стабильная версия)

Мониторинг и регистрация

async createContact(params: any, authData?: OptiIdAuthData) {
  const startTime = Date.now();
  
  try {
    console.log('[ActiveCampaign] Creating contact:', { email: params.email });
    const client = this.getClient(authData);
    const contact = await client.createContact(params);
    
    const duration = Date.now() - startTime;
    console.log(`[ActiveCampaign] Contact created in ${duration}ms:`, contact.id);
    
    return { success: true, contact };
  } catch (error: any) {
    console.error('[ActiveCampaign] Create contact failed:', {
      error: error.message,
      status: error.response?.status,
      duration: Date.now() - startTime
    });
    throw error;
  }
}

Ограничение скорости

Соблюдайте ограничения скорости API:

import pLimit from 'p-limit';

export class ActiveCampaignClient {
  private rateLimiter = pLimit(5); // 5 concurrent requests max

  async createContact(data: any): Promise {
    return this.rateLimiter(async () => {
      const response = await this.client.post('/api/3/contacts', {
        contact: data
      });
      return response.data.contact;
    });
  }
}

Тестирование вашего инструмента

Контрольный список ручного тестирования

  • Инструмент появляется в списке инструментов Opal после добавления URL-адреса обнаружения.
  • Форма настроек успешно сохраняет учетные данные
  • Каждая функция инструмента возвращает ожидаемые данные.
  • Сообщения об ошибках понятны и понятны.
  • Ограничения скорости не приводят к сбоям
  • Большие наборы данных обрабатываются изящно

Заключение

Теперь у вас есть полное представление о создании инструментов Opal с помощью OCP SDK. Ключевые выводы:

  1. Используйте @optimizely-opal/opal-tool-ocp-sdk. для новейших моделей инструментов Opal
  2. Следуйте шаблону типа функции opal_tool. в app.yml для автоматического управления учетными данными
  3. Четко структурируйте свой код: API-клиент → Функции инструмента → Обработчики жизненного цикла
  4. Используйте TypeScript для лучшего опыта разработчика и меньшего количества ошибок во время выполнения
  5. Тщательно протестируйте перед переходом от версии для разработчиков к производственной версии
  6. Добавьте разрешение grpc-boom чтобы избежать сбоев сборки

Ресурсы

Документация OCP SDK
Пример репозитория инструментов Opal
Документация по API ActiveCampaign
Сообщество Оптимизации

Приятного строительства! Если вы создадите инструмент Opal, используя это руководство, поделитесь им с сообществом Optimizely. Чем больше инструментов доступно, тем более мощным становится Opal для всех.

08 января 2026 г.

По теме

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.