اكتشاف المكونات الإضافية وتسجيلها
سجلات الأدوات، وملفات البيان، والتحميل الديناميكي للأدوات أثناء التشغيل.
اكتشاف المكونات الإضافية وتسجيلها درس مجاني في 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 مع name وversion وentry_point وtool_definitions وrequires
- الاكتشاف: فحص الدليل بحثًا عن أدلة فرعية تحتوي على plugin.json
- التحقق: فحص الحقول المطلوبة وتنسيق الإصدار ومتغيرات البيئة وعدم فراغ الأدوات
- التحميل الديناميكي: استخدام importlib.import_module() مع مسار entry_point
- سجل الأدوات: خريطة مركزية من اسم الأداة إلى المخطط + دالة التنفيذ
- إعادة التحميل الفوري: تراقب watchdog دليل الإضافات وتعيد التحميل عند تغير البيان
التالي: إدارة إصدارات الأدوات والتوافق بينها.
الأسئلة الشائعة
هل درس «اكتشاف المكونات الإضافية وتسجيلها» مجاني؟
نعم — نص درس «اكتشاف المكونات الإضافية وتسجيلها» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 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 يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- تصميم أدوات وكلاء قابلة للمشاركة
- اكتشاف المكونات الإضافية وتسجيلها
- إصدار الأدوات وتوافقها
- بناء سوق للمكونات الإضافية للوكلاء