Descoberta e registro de plug-ins
Registros de ferramentas, arquivos de manifesto e carregamento dinâmico de ferramentas em tempo de execução.
Descoberta e registro de plug-ins é uma aula grátis de AI Agents no CoddyKit. Esta é a aula 2 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de AI Agents, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de AI Agents inclui 4 aulas no total.
Sistemas de plugins para agentes
Um sistema de plugins permite que os agentes descubram e carreguem novas ferramentas em tempo de execução sem modificar o código principal do agente. O agente lê um diretório de plugins, carrega o manifesto de cada plugin, valida-o e adiciona suas ferramentas ao registro de ferramentas ativo.
Isso possibilita arquiteturas de agentes modulares e extensíveis.
Formato do manifesto de plugin
Todo plugin inclui um arquivo de manifesto plugin.json. Ele é o cartão de identidade do plugin: informa o que ele é, qual é sua versão, quais ferramentas fornece e do que precisa (pacotes Python e variáveis de ambiente).
# 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])Carregador de diretório de plugins
Examine um diretório em busca de subdiretórios de plugins. Cada subdiretório deve conter um manifesto plugin.json. O carregador lê cada manifesto, valida-o e registra o plugin no registro de plugins.
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)}')Validação do manifesto
Antes de carregar um plugin, valide seu manifesto em relação a um esquema para detectar campos obrigatórios ausentes, versões inválidas ou variáveis de ambiente ausentes. Rejeite plugins inválidos e registre o motivo.
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))
Carregamento dinâmico de ferramentas
Depois que o manifesto de um plugin for validado, importe dinamicamente o módulo Python do plugin e chame get_tools() para obter as definições das ferramentas e as funções de execução. O importlib do Python torna esse processo simples.
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'])
Registro de ferramentas
O registro de ferramentas é a fonte única da verdade para todas as ferramentas disponíveis. Ele associa os nomes das ferramentas aos seus esquemas e funções de execução. O agente consulta o registro ao criar sua lista de ferramentas para cada chamada ao 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'}))
Pesquisa por capacidade
Quando um agente tem muitos plugins, ele deve ser capaz de pesquisar no registro por uma etiqueta de capacidade, em vez de carregar todas as ferramentas em cada chamada ao LLM. Um número excessivo de ferramentas no contexto reduz a precisão da seleção de ferramentas pelo 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]}')Recarregamento a quente após alterações no diretório
Durante o desenvolvimento, é útil recarregar os plugins quando os arquivos forem alterados, sem reiniciar o agente. Utilize a biblioteca watchdog para observar o diretório de plugins e iniciar um recarregamento quando os arquivos plugin.json forem alterados.
# 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 observerVerificação e instalação automática de dependências
Ao carregar um plugin, verifique se os pacotes Python necessários estão instalados. Opcionalmente, instale automaticamente os pacotes ausentes usando pip. Registre um erro claro se não for possível satisfazer uma dependência.
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)
Inicialização completa do plugin
Reúna a descoberta, a validação, a verificação de dependências, o carregamento do módulo e o registro no registro de ferramentas em uma única função de inicialização chamada na inicialização do agente.
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 resultsRegistrando eventos do ciclo de vida dos plugins
Registre todos os eventos significativos do ciclo de vida do plugin: descoberta, falha de validação, carregamento bem-sucedido, recarregamento a quente e descarregamento. Esses registros são essenciais para depurar e auditar o sistema de plugins em produção.
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']})
Verificação de conhecimentos
Qual é a finalidade do campo entry_point no manifesto de um plugin?
Recapitulação: descoberta e registro de plugins
Excelente! O que você aprendeu:
- Manifesto do plugin: plugin.json com nome, versão, entry_point, tool_definitions e requires
- Descoberta: examinar o diretório em busca de subdiretórios com plugin.json
- Validação: verificar campos obrigatórios, formato da versão, variáveis de ambiente e ferramentas não vazias
- Carregamento dinâmico: importlib.import_module() usando o caminho entry_point
- Registro de ferramentas: associação central do nome da ferramenta ao esquema e à função execute
- Recarregamento a quente: watchdog observa o diretório de plugins e recarrega após alterações no manifesto
Próximo tópico: versionamento de ferramentas e gerenciamento de compatibilidade.
Perguntas Frequentes
A aula “Descoberta e registro de plug-ins” é grátis?
Sim — o texto completo de “Descoberta e registro de plug-ins” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de AI Agents, atualize para CoddyKit PRO. O curso de AI Agents inclui 4 aulas no total.
O que vou aprender em “Descoberta e registro de plug-ins”?
Registros de ferramentas, arquivos de manifesto e carregamento dinâmico de ferramentas em tempo de execução. Você pratica AI Agents com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.
Preciso ter experiência prévia para começar AI Agents?
Nenhuma experiência prévia é necessária. AI Agents no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 2 de 4.
Quanto tempo leva a aula “Descoberta e registro de plug-ins”?
A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.
Posso escrever e executar código nesta aula de AI Agents?
Sim. Cada aula de AI Agents inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.
Todas as aulas deste curso
- Projetando ferramentas de agentes reutilizáveis
- Descoberta e registro de plug-ins
- Versionamento e compatibilidade de ferramentas
- Construindo um marketplace de plug-ins para agentes