AI Agents · Урок

Обнаружение и регистрация плагинов

Реестры инструментов, файлы манифестов и динамическая загрузка инструментов во время выполнения.

Урок 2 из 413 шагов

«Обнаружение и регистрация плагинов» — бесплатный урок AI Agents на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения AI Agents, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс AI Agents содержит 4 уроков всего.

Системы плагинов для агентов

Система плагинов позволяет агентам обнаруживать и загружать новые инструменты во время работы без изменения основного кода агента. Агент читает каталог плагинов, загружает манифест каждого плагина, проверяет его и добавляет инструменты в активный реестр инструментов.

Это позволяет создавать модульные и расширяемые архитектуры агентов.

Формат манифеста плагина

Каждый плагин поставляется с файлом манифеста plugin.json. Это удостоверение плагина: в нём указано, что это за плагин, какова его версия, какие инструменты он предоставляет и какие требования предъявляет (пакеты Python, переменные окружения).

# plugin.json — stored in the plugin's root directory
EXAMPLE_MANIFEST = {
    'name': 'weather-tools',
    'display_name': 'Weather Tools',
    'version': '2.1.0',
    'description': 'Real-time weather and forecast tools',
    'author': 'Jane Developer <jane@example.com>',
    'license': 'MIT',
    'entry_point': 'weather_tools.plugin',  # Python module path
    'tool_definitions': [
        'get_current_weather',
        'get_5day_forecast',
        'get_weather_alerts'
    ],
    'requires': {
        'python_packages': ['requests>=2.28'],
        'env_vars': ['WEATHER_API_KEY']
    },
    'tags': ['weather', 'forecast', 'iot'],
    'min_framework_version': '1.0.0'
}

import json
print(json.dumps(EXAMPLE_MANIFEST, indent=2)[:300])

Загрузчик каталога плагинов

Сканируйте каталог в поисках подкаталогов плагинов. Каждый подкаталог должен содержать манифест plugin.json. Загрузчик читает каждый манифест, проверяет его и регистрирует плагин в реестре плагинов.

import os
import json

PLUGIN_DIR = 'plugins'

def discover_plugins(plugin_dir: str = PLUGIN_DIR) -> list:
    discovered = []
    if not os.path.isdir(plugin_dir):
        print(f'Plugin directory not found: {plugin_dir}')
        return []

    for entry in os.scandir(plugin_dir):
        if not entry.is_dir():
            continue
        manifest_path = os.path.join(entry.path, 'plugin.json')
        if not os.path.exists(manifest_path):
            print(f'Skipping {entry.name}: no plugin.json')
            continue
        try:
            with open(manifest_path) as f:
                manifest = json.load(f)
            manifest['_path'] = entry.path
            manifest['_name'] = entry.name
            discovered.append(manifest)
            print(f'Discovered: {manifest["name"]} v{manifest["version"]}')
        except (json.JSONDecodeError, KeyError) as e:
            print(f'Invalid manifest in {entry.name}: {e}')

    return discovered

plugins = discover_plugins()
print(f'Total plugins discovered: {len(plugins)}')

Проверка манифеста

Перед загрузкой плагина проверяйте его манифест по схеме, чтобы обнаружить отсутствующие обязательные поля, недопустимые версии или отсутствующие переменные окружения. Отклоняйте недопустимые плагины и записывайте причину в журнал.

import os

REQUIRED_MANIFEST_FIELDS = {'name', 'version', 'entry_point', 'tool_definitions'}

def validate_manifest(manifest: dict) -> tuple:
    """
    Returns (is_valid: bool, errors: list)
    """
    errors = []

    # Required fields
    missing = REQUIRED_MANIFEST_FIELDS - set(manifest.keys())
    if missing:
        errors.append(f'Missing required fields: {missing}')

    # Version format
    version = manifest.get('version', '')
    parts = version.split('.')
    if len(parts) != 3 or not all(p.isdigit() for p in parts):
        errors.append(f'Invalid version format: {version}')

    # Check required env vars exist
    env_vars = manifest.get('requires', {}).get('env_vars', [])
    for var in env_vars:
        if not os.environ.get(var):
            errors.append(f'Missing required env var: {var}')

    # Tool definitions must be a non-empty list
    tools = manifest.get('tool_definitions', [])
    if not isinstance(tools, list) or len(tools) == 0:
        errors.append('tool_definitions must be a non-empty list')

    return len(errors) == 0, errors

if __name__ == '__main__':
    good_manifest = {
        'name': 'weather-tools', 'version': '1.2.0',
        'entry_point': 'weather_tools.plugin', 'tool_definitions': [{'name': 'get_weather'}]
    }
    bad_manifest = {'name': 'broken-plugin', 'version': 'v1'}
    print('Good manifest:', validate_manifest(good_manifest))
    print('Bad manifest: ', validate_manifest(bad_manifest))

Динамическая загрузка инструментов

После проверки манифеста плагина динамически импортируйте его модуль Python и вызовите get_tools(), чтобы получить определения инструментов и функции их выполнения. Механизм importlib в Python делает это довольно просто.

import importlib
import importlib.util
import sys

def load_plugin_module(manifest: dict):
    """
    Import the plugin's Python module and return it.
    Adds the plugin directory to sys.path if needed.
    """
    plugin_path = manifest['_path']
    entry_point = manifest['entry_point']  # e.g. 'weather_tools.plugin'

    # Add plugin directory to path so relative imports work
    if plugin_path not in sys.path:
        sys.path.insert(0, plugin_path)

    try:
        module = importlib.import_module(entry_point)
        return module
    except ImportError as e:
        print(f'Failed to import {entry_point}: {e}')
        return None

def extract_tools_from_module(module, tool_names: list) -> dict:
    """
    Returns {tool_name: {'schema': dict, 'execute': callable}}
    """
    tools = {}
    for name in tool_names:
        schema_fn = getattr(module, f'get_{name}_schema', None)
        execute_fn = getattr(module, f'execute_{name}', None)
        if schema_fn and execute_fn:
            tools[name] = {'schema': schema_fn(), 'execute': execute_fn}
    return tools

if __name__ == '__main__':
    class FakeModule:
        def get_weather_schema(self):
            return {'name': 'get_weather'}
        def execute_weather(self, params):
            return {'temp': 72}

    tools = extract_tools_from_module(FakeModule(), ['weather'])
    print('Extracted tools:', list(tools.keys()))
    print('Weather schema:', tools['weather']['schema'])

Реестр инструментов

Реестр инструментов — это единый достоверный источник сведений обо всех доступных инструментах. Он сопоставляет имена инструментов с их схемами и функциями выполнения. Агент обращается к реестру при формировании списка инструментов для каждого вызова LLM.

class ToolRegistry:
    def __init__(self):
        self._tools: dict = {}  # name -> {'schema', 'execute', 'plugin'}
        self._plugins: dict = {}  # plugin_name -> manifest

    def register_tool(
        self, name: str, schema: dict,
        execute_fn, plugin_name: str
    ):
        if name in self._tools:
            print(f'WARNING: Tool {name} already registered, overwriting')
        self._tools[name] = {
            'schema': schema,
            'execute': execute_fn,
            'plugin': plugin_name
        }

    def register_plugin(self, manifest: dict, tools: dict):
        self._plugins[manifest['name']] = manifest
        for name, tool in tools.items():
            self.register_tool(name, tool['schema'],
                               tool['execute'], manifest['name'])
        print(f'Registered plugin: {manifest["name"]} '
              f'({len(tools)} tools)')

    def get_all_schemas(self) -> list:
        return [t['schema'] for t in self._tools.values()]

    def execute(self, tool_name: str, params: dict):
        if tool_name not in self._tools:
            raise KeyError(f'Unknown tool: {tool_name}')
        return self._tools[tool_name]['execute'](params)

registry = ToolRegistry()

if __name__ == '__main__':
    def get_weather(params):
        return {'temp': 72, 'city': params.get('city')}

    manifest = {'name': 'weather-tools'}
    tools = {'get_weather': {'schema': {'name': 'get_weather'}, 'execute': get_weather}}
    registry.register_plugin(manifest, tools)
    print('Registered schemas:', registry.get_all_schemas())
    print('Execution result:', registry.execute('get_weather', {'city': 'Paris'}))

Поиск по возможностям

Если у агента много плагинов, он должен уметь искать в реестре по тегу возможностей, а не загружать все инструменты в каждый вызов LLM. Слишком большое количество инструментов в контексте снижает точность выбора инструментов LLM.

class SearchableToolRegistry(ToolRegistry):
    def search(self, query: str) -> list:
        """
        Search tools by name, description, or tags.
        Returns list of matching tool schemas.
        """
        query_lower = query.lower()
        matches = []
        for name, tool in self._tools.items():
            schema = tool['schema']
            plugin = self._plugins.get(tool['plugin'], {})
            plugin_tags = plugin.get('tags', [])

            if (
                query_lower in name.lower() or
                query_lower in schema.get('description', '').lower() or
                any(query_lower in tag.lower() for tag in plugin_tags)
            ):
                matches.append(schema)
        return matches

# Usage:
registry = SearchableToolRegistry()
# After loading plugins...
weather_tools = registry.search('weather')
print(f'Weather tools: {[t["name"] for t in weather_tools]}')

Горячая перезагрузка при изменении каталога

В процессе разработки удобно перезагружать плагины при изменении файлов, не перезапуская агента. Используйте библиотеку watchdog, чтобы отслеживать каталог плагинов и запускать перезагрузку при изменении файлов plugin.json.

# pip install watchdog
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class PluginReloadHandler(FileSystemEventHandler):
    def __init__(self, registry, plugin_loader_fn):
        self.registry = registry
        self.loader = plugin_loader_fn

    def on_modified(self, event):
        if 'plugin.json' in event.src_path:
            print(f'Plugin manifest changed: {event.src_path}')
            self.reload_plugin(event.src_path)

    def reload_plugin(self, manifest_path: str):
        import json, os
        plugin_dir = os.path.dirname(manifest_path)
        try:
            with open(manifest_path) as f:
                manifest = json.load(f)
            manifest['_path'] = plugin_dir
            self.loader(manifest, self.registry)
            print(f'Reloaded: {manifest["name"]}')
        except Exception as e:
            print(f'Reload failed: {e}')

def start_plugin_watcher(plugin_dir: str, registry):
    handler = PluginReloadHandler(registry, lambda m, r: None)
    observer = Observer()
    observer.schedule(handler, plugin_dir, recursive=True)
    observer.start()
    return observer

Проверка зависимостей и автоматическая установка

При загрузке плагина проверяйте, установлены ли необходимые ему пакеты Python. При необходимости автоматически устанавливайте отсутствующие пакеты с помощью pip. Записывайте понятную ошибку в журнал, если зависимость невозможно удовлетворить.

import subprocess
import sys
import importlib

def check_and_install_deps(manifest: dict, auto_install: bool = False) -> bool:
    packages = manifest.get('requires', {}).get('python_packages', [])
    missing = []

    for pkg_spec in packages:
        pkg_name = pkg_spec.split('>=')[0].split('==')[0].strip()
        try:
            importlib.import_module(pkg_name.replace('-', '_'))
        except ImportError:
            missing.append(pkg_spec)

    if not missing:
        return True

    print(f'Missing packages for {manifest["name"]}: {missing}')

    if auto_install:
        for pkg in missing:
            print(f'Installing {pkg}...')
            result = subprocess.run(
                [sys.executable, '-m', 'pip', 'install', pkg],
                capture_output=True, text=True
            )
            if result.returncode != 0:
                print(f'Install failed: {result.stderr[:200]}')
                return False
        return True

    return False

if __name__ == '__main__':
    manifest = {'name': 'demo-plugin', 'requires': {'python_packages': ['totally_fake_package_xyz']}}
    ok = check_and_install_deps(manifest, auto_install=False)
    print('All dependencies satisfied:', ok)

Полная начальная загрузка плагинов

Объедините обнаружение, проверку, проверку зависимостей, загрузку модулей и регистрацию в реестре в одной функции начальной загрузки, вызываемой при запуске агента.

def bootstrap_plugins(
    plugin_dir: str,
    registry: ToolRegistry,
    auto_install_deps: bool = False
) -> dict:
    results = {'loaded': [], 'failed': []}
    manifests = discover_plugins(plugin_dir)

    for manifest in manifests:
        name = manifest.get('name', '?')
        is_valid, errors = validate_manifest(manifest)
        if not is_valid:
            print(f'INVALID {name}: {errors}')
            results['failed'].append({'name': name, 'reason': errors})
            continue

        deps_ok = check_and_install_deps(manifest, auto_install_deps)
        if not deps_ok:
            results['failed'].append({'name': name, 'reason': 'missing_deps'})
            continue

        module = load_plugin_module(manifest)
        if not module:
            results['failed'].append({'name': name, 'reason': 'import_error'})
            continue

        tools = extract_tools_from_module(module, manifest['tool_definitions'])
        registry.register_plugin(manifest, tools)
        results['loaded'].append(name)

    print(f'Plugins loaded: {results["loaded"]}')
    print(f'Plugins failed: {results["failed"]}')
    return results

Журналирование событий жизненного цикла плагинов

Записывайте в журнал каждое значимое событие жизненного цикла плагина: обнаружение, ошибку проверки, успешную загрузку, горячую перезагрузку и выгрузку. Эти журналы необходимы для отладки и аудита системы плагинов в рабочей среде.

import logging
from datetime import datetime

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
plugin_logger = logging.getLogger('plugin_system')

def log_plugin_event(event: str, plugin_name: str, details: dict = None):
    entry = {
        'event': event,
        'plugin': plugin_name,
        'timestamp': datetime.utcnow().isoformat(),
        'details': details or {}
    }
    if event in ('VALIDATION_FAILED', 'LOAD_FAILED', 'SIGNATURE_INVALID'):
        plugin_logger.warning('Plugin event: %s', entry)
    else:
        plugin_logger.info('Plugin event: %s', entry)

# Usage:
log_plugin_event('DISCOVERED', 'weather-tools', {'path': 'plugins/weather-tools'})
log_plugin_event('LOADED', 'weather-tools', {'tools': ['get_current_weather']})
log_plugin_event('VALIDATION_FAILED', 'bad-plugin', {'errors': ['Missing entry_point']})

if __name__ == '__main__':
    import sys
    plugin_logger.addHandler(logging.StreamHandler(sys.stdout))
    log_plugin_event('LOADED', 'demo-plugin', {'tools': ['get_weather']})

Проверка знаний

Каково назначение поля entry_point в манифесте плагина?

Повторение: обнаружение и регистрация плагинов

Отлично! Вы изучили следующее:

  • Манифест плагина: plugin.json с именем, версией, точкой входа, определениями инструментов и требованиями
  • Обнаружение: сканирование каталога в поисках подкаталогов с plugin.json
  • Проверка: проверка обязательных полей, формата версии, переменных окружения и наличия инструментов
  • Динамическая загрузка: importlib.import_module() с использованием пути к точке входа
  • Реестр инструментов: центральное сопоставление имени инструмента со схемой и функцией выполнения
  • Горячая перезагрузка: библиотека watchdog отслеживает каталог плагинов и перезагружает их при изменении манифеста

Далее: версионирование инструментов и управление совместимостью.

Можно начать бесплатно

Изучай AI Agents с ИИ-репетитором — бесплатно

Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.

Курсы
60
Уроки
239

Часто задаваемые вопросы

Урок «Обнаружение и регистрация плагинов» бесплатный?

Да — полный текст урока «Обнаружение и регистрация плагинов» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс AI Agents, подпишись на CoddyKit PRO. Курс AI Agents содержит 4 уроков всего.

Чему я научусь в уроке «Обнаружение и регистрация плагинов»?

Реестры инструментов, файлы манифестов и динамическая загрузка инструментов во время выполнения. Ты практикуешь AI Agents с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать AI Agents?

Предыдущий опыт не требуется. AI Agents на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.

Сколько времени занимает урок «Обнаружение и регистрация плагинов»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке AI Agents?

Да. Каждый урок AI Agents включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Проектирование инструментов агентов для совместного использования
  2. Обнаружение и регистрация плагинов
  3. Версионирование инструментов и совместимость
  4. Создание рынка плагинов для агентов
← Назад к AI Agents