0Pricing
AI Agents · 강의

플러그인 검색 및 등록

도구 레지스트리, 매니페스트 파일, 실행 중 동적 도구 로딩을 학습합니다.

플러그인 검색 및 등록은(는) CoddyKit의 무료 AI Agents 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 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()를 호출하여 도구 정의와 실행 함수를 가져옵니다. Python의 importlib를 사용하면 이 작업을 간단히 수행할 수 있습니다.

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가 플러그인 디렉터리를 감시하고 매니페스트 변경 시 다시 로드

다음 주제: 도구 버전 관리 및 호환성 관리

자주 묻는 질문

“플러그인 검색 및 등록” 강의는 무료인가요?

네 — “플러그인 검색 및 등록” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Agents 강의 전체를 잠금 해제할 수 있습니다. AI Agents 강의에는 총 4개의 강의가 포함되어 있습니다.

“플러그인 검색 및 등록”에서 뭘 배우나요?

도구 레지스트리, 매니페스트 파일, 실행 중 동적 도구 로딩을 학습합니다. 브라우저에서 직접 실행하는 실습 코드로 AI Agents을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

AI Agents을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 AI Agents은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“플러그인 검색 및 등록” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 AI Agents 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 AI Agents 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 공유 가능한 에이전트 도구 설계
  2. 플러그인 검색 및 등록
  3. 도구 버전 관리 및 호환성
  4. 에이전트 플러그인 마켓플레이스 구축
← AI Agents(으)로 돌아가기