플러그인 검색 및 등록
도구 레지스트리, 매니페스트 파일, 실행 중 동적 도구 로딩을 학습합니다.
플러그인 검색 및 등록은(는) 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 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.