Escribir un plugin¶
Los plugins extienden AdaptiveLearner sin tocar el núcleo. El sistema de plugins usa PluginForge (un envoltorio sobre pluggy). Cada plugin es un paquete Poetry independiente registrado mediante un punto de entrada.
Este tutorial explica cómo crear un plugin «hello-world» que añade una ruta y escucha un hook.
1. Crear la estructura de directorios¶
mkdir -p plugins/adaptive-learner-plugin-hello/adaptive_learner_hello
mkdir -p plugins/adaptive-learner-plugin-hello/tests
cd plugins/adaptive-learner-plugin-hello
2. pyproject.toml¶
[tool.poetry]
name = "adaptive-learner-plugin-hello"
version = "1.0.0"
description = "Adaptive Learner: plugin hello-world"
authors = ["Tu Nombre"]
license = "MIT"
[tool.poetry.dependencies]
python = "^3.11"
pluginforge = "^0.10.0"
fastapi = "^0.136.0"
[project.entry-points."adaptive_learner.plugins"]
hello = "adaptive_learner_hello.plugin:HelloPlugin"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
El nombre del punto de entrada (hello aquí) es lo que usa el
registro de plugins para rastrear el plugin. La ruta de la clase
(adaptive_learner_hello.plugin:HelloPlugin) es la ruta de
importación Python a la clase del plugin.
3. plugin.py¶
from pluginforge import BasePlugin, hookimpl
from typing import Any
class HelloPlugin(BasePlugin):
name = "hello"
version = "1.0.0"
# Verificación de identidad pluginforge ^0.10.0. Fíjalo en
# "adaptive_learner" para que el PluginManager del host (que
# pasa ``app_id="adaptive_learner"``) reconozca el plugin como
# dirigido a esta aplicación. La transición v0.9.0 convirtió
# esto en un FILTRO DURO — los plugins sin esto son rechazados
# en el momento del descubrimiento.
target_application = "adaptive_learner"
depends_on: list[str] = []
@hookimpl
def on_session_complete(
self, session: dict[str, Any], rating: dict[str, Any]
) -> None:
print(f"¡Hola! La sesión {session['id']} ha terminado.")
BasePlugin es la clase base de PluginForge. El decorador
@hookimpl marca un método como implementación de un hook
especificado en backend/app/hookspecs.py.
4. routes.py¶
from fastapi import APIRouter
router = APIRouter(prefix="/api/plugins/hello", tags=["hello"])
@router.get("/greet")
def greet() -> dict:
return {"message": "¡Hola desde el plugin hello!"}
5. Registrar las rutas en plugin.py¶
from fastapi import FastAPI
from .routes import router
class HelloPlugin(BasePlugin):
...
def mount_routes(self, app: FastAPI) -> None:
app.include_router(router)
El gestor de plugins llama a mount_routes() para cada plugin
que lo define, después de que la aplicación núcleo está
inicializada.
6. Pruebas¶
# tests/test_plugin.py
from adaptive_learner_hello.plugin import HelloPlugin
def test_hello_plugin_has_name():
plugin = HelloPlugin()
assert plugin.name == "hello"
7. Instalar + activar¶
Añade el plugin como dependencia de ruta en backend/pyproject.toml:
[tool.poetry.dependencies]
...
adaptive-learner-plugin-hello = {path = "../plugins/adaptive-learner-plugin-hello", develop = true}
Luego:
cd backend && poetry lock && poetry install
make dev
curl http://localhost:18001/api/plugins/hello/greet
Los 10 hookspecs¶
Todas las especificaciones de hooks viven en
backend/app/hookspecs.py:
get_assessment_questions(lang: str)— devuelve el paquete de preguntas.calculate_profile(answers: list)— calcula los pesos de los métodos (firstresult).create_session_prompt(...)— compone el prompt del sistema (firstresult).ai_complete(messages, model, api_key, max_tokens)— llama a la IA de forma síncrona (firstresult, el proveedor enruta por prefijo de modelo).ai_complete_async(...)— variante asíncrona para la evaluación paralela en el límite del ciclo (v1.5.0+, firstresult).ai_complete_stream(...)— variante en streaming que produce deltas de texto mediante SSE (v1.6.0+, firstresult).recommend_method_switch(history, profile)— devuelve una recomendación de cambio o None.on_session_complete(session, rating)— efecto secundario broadcast; la gamificación y el seguimiento escuchan.get_progress_summary(project_id, db)— devuelve un fragmento de espacio de nombres del resumen del panel.get_tool_recommendations(profile, lang)— devuelve herramientas ordenadas.
Referencia completa de hookspecs
Semántica de firstresult¶
Los hooks marcados con firstresult=True se detienen en el
primer plugin que devuelve un valor distinto de None. Útil para
los casos de «exactamente un plugin debe manejar esto» — como
ai_complete, donde el plugin del proveedor correspondiente
devuelve el texto y los demás devuelven None.
Los hooks sin firstresult=True se ejecutan en cada plugin (modo
lista). El llamador recibe una lista de resultados y decide qué
hacer (fusionar, preferir, etc.).