Escrever um plugin¶
Os plugins estendem o AdaptiveLearner sem tocar no núcleo. O sistema de plugins usa o PluginForge (um invólucro em torno do pluggy). Cada plugin é um pacote Poetry independente registado via um ponto de entrada.
Este tutorial apresenta a criação de um plugin "hello-world" que adiciona uma rota e ouve um hook.
1. Estruturar o diretório¶
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: hello-world plugin"
authors = ["Your Name"]
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"
O nome do ponto de entrada (hello aqui) é o que o registo de
plugins usa para rastrear o plugin. O caminho de classe
(adaptive_learner_hello.plugin:HelloPlugin) é o caminho de
importação Python para a classe do plugin.
3. plugin.py¶
from pluginforge import BasePlugin, hookimpl
from typing import Any
class HelloPlugin(BasePlugin):
name = "hello"
version = "1.0.0"
# Restrição de identidade pluginforge ^0.10.0. Defina isto como
# "adaptive_learner" para que o PluginManager do host (que
# passa ``app_id="adaptive_learner"``) reconheça o plugin como
# direcionado para esta aplicação. A transição v0.9.0 tornou
# isto um filtro RIGOROSO — plugins sem isto são rejeitados
# no momento da descoberta.
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"Hello! Session {session['id']} ended.")
BasePlugin é a classe base do PluginForge. O decorador
@hookimpl marca um método como implementando um hook especificado
em 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": "Hello from the hello plugin!"}
5. Registar rotas em plugin.py¶
from fastapi import FastAPI
from .routes import router
class HelloPlugin(BasePlugin):
...
def mount_routes(self, app: FastAPI) -> None:
app.include_router(router)
O gestor de plugins chama mount_routes() para cada plugin que
o define, depois de a aplicação principal estar inicializada.
6. Testes¶
# 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 + ativar¶
Adicione o plugin como dependência de caminho em
backend/pyproject.toml:
[tool.poetry.dependencies]
...
adaptive-learner-plugin-hello = {path = "../plugins/adaptive-learner-plugin-hello", develop = true}
Depois:
cd backend && poetry lock && poetry install
make dev
curl http://localhost:18001/api/plugins/hello/greet
Os 10 hookspecs¶
Todas as especificações de hooks vivem em
backend/app/hookspecs.py:
get_assessment_questions(lang: str)— retornar pacote de perguntas.calculate_profile(answers: list)— calcular pesos de método (firstresult).create_session_prompt(...)— compor o prompt do sistema (firstresult).ai_complete(messages, model, api_key, max_tokens)— chamar a IA de forma síncrona (firstresult, fornecedor rotas por prefixo de modelo).ai_complete_async(...)— variante async para avaliação paralela em limite de ciclo (v1.5.0+, firstresult).ai_complete_stream(...)— variante em streaming emitindo deltas de texto via SSE (v1.6.0+, firstresult).recommend_method_switch(history, profile)— retornar uma recomendação de mudança ou None.on_session_complete(session, rating)— efeito secundário de broadcast; gamificação + rastreamento ouvem.get_progress_summary(project_id, db)— retornar uma fatia de espaço de nomes do resumo do dashboard.get_tool_recommendations(profile, lang)— retornar ferramentas classificadas.
Referência completa de hookspecs
Semântica firstresult¶
Os hooks marcados com firstresult=True param no primeiro plugin
que retorna um valor não-None. Útil para casos "exatamente um
plugin deve tratar disto" — como ai_complete, onde o plugin do
fornecedor correspondente retorna o texto e os outros retornam None.
Os hooks sem firstresult=True correm em cada plugin (modo lista).
O chamador recebe uma lista de resultados e decide o que fazer
(fundir, preferir, etc.).