Especificaciones de hooks¶
Los 10 hookspecs viven en backend/app/hookspecs.py. Cada
hookspec define el contrato de llamada; los plugins los implementan
con @hookimpl. Tres de ellos son variantes de llamada de IA
(síncrona / asíncrona / streaming) introducidas progresivamente en
v1.5.0 y v1.6.0; el resto no ha cambiado desde v0.2.0.
get_assessment_questions¶
@hookspec
def get_assessment_questions(lang: str) -> list[dict] | None:
"""Devuelve el paquete de preguntas para el idioma solicitado.
Implementado por: plugin de evaluación.
Modo: list (no firstresult). La ruta actualmente usa el resultado
del primer plugin; una futura funcionalidad de "múltiples paquetes
de preguntas" permitiría a los proveedores registrar paquetes nombrados.
"""
Forma del valor devuelto:
[
{
"id": "q01",
"type": "single" | "multi",
"text": "...",
"answers": [
{"id": "a", "text": "...", "weights": {"deductive": 1.0}},
...
]
},
...
]
calculate_profile¶
@hookspec(firstresult=True)
def calculate_profile(answers: list[dict]) -> dict[str, float]:
"""Agrega las respuestas brutas en un perfil de 6 métodos.
Implementado por: plugin de evaluación.
firstresult: exactamente un plugin calcula el perfil.
"""
Forma de answers de entrada:
[
{"question_id": "q01", "answer_ids": ["a", "b"]}, # multi
{"question_id": "q02", "answer_id": "c"}, # single
...
]
Devuelve: dict[method, float] con las seis claves de método.
create_session_prompt¶
@hookspec(firstresult=True)
def create_session_prompt(
project: dict,
profile: dict,
method: str,
step: int,
lang: str,
) -> str:
"""Compone el prompt de sistema para una celda (método, paso, idioma).
Implementado por: plugin de sesión.
firstresult: exactamente un plugin compone el prompt.
"""
Devuelve: una cadena lista para enviarse como mensaje de rol system.
La implementación del plugin de sesión lee del dict _PROMPTS de 42
celdas y añade un bloque de contexto.
ai_complete¶
@hookspec(firstresult=True)
def ai_complete(
messages: list[dict[str, Any]],
model: str,
api_key: str,
max_tokens: int = 1024,
) -> str | None:
"""Llama al proveedor de IA, devuelve el texto del asistente.
Implementado por: ai-anthropic, ai-openai, ai-gemini.
firstresult: el plugin del proveedor correspondiente devuelve el
texto; los demás devuelven None.
"""
Cada plugin de proveedor comprueba el prefijo de model
(claude-*, gpt-*, gemini-*) y devuelve el texto del asistente
si es el propietario del modelo. Los plugins no coincidentes devuelven
None, dejando que firstresult pase al siguiente.
Forma de messages (estilo OpenAI):
[
{"role": "system", "content": "..."},
{"role": "user", "content": "..."},
{"role": "assistant", "content": "..."},
{"role": "user", "content": "..."},
]
Los plugins de proveedor normalizan esta forma a su propia API
(Anthropic separa system; Gemini lo integra).
ai_complete_async (v1.5.0+)¶
@hookspec(firstresult=True)
async def ai_complete_async(
messages: list[dict[str, Any]],
model: str,
api_key: str,
max_tokens: int = 1024,
) -> str:
"""Variante awaitable de ai_complete.
Implementado por: ai-anthropic, ai-openai, ai-gemini.
Usado en el límite de ciclo paso 6 → 7 para que la evaluación de paso +
transición de tema se disparen de forma concurrente mediante
asyncio.gather (ahorra ~T_2 de latencia).
"""
El helper orquestador call_ai_complete_async prefiere este hook;
recurre a ai_complete envuelto en asyncio.to_thread cuando no
está implementado.
ai_complete_stream (v1.6.0+)¶
@hookspec(firstresult=True)
def ai_complete_stream(
messages: list[dict[str, Any]],
model: str,
api_key: str,
max_tokens: int = 1024,
) -> AsyncIterator[str]:
"""Devuelve un iterador asíncrono de deltas de texto.
Implementado por: ai-anthropic, ai-openai, ai-gemini, cada uno
usando el streaming asíncrono nativo del SDK del proveedor.
Alimenta ``POST /api/plugins/session/{id}/message/stream``
(SSE: emite eventos ``start`` / ``chunk`` / ``done``).
"""
recommend_method_switch¶
@hookspec
def recommend_method_switch(
history: list[dict],
profile: dict,
) -> dict | None:
"""Devuelve una recomendación de cambio, o None.
Implementado por: plugin de sesión.
Modo: list. Los plugins devuelven recomendaciones individuales;
un futuro árbitro elegirá la de mayor confianza distinta de None.
"""
Forma del valor devuelto:
{
"recommended": True,
"to_method": "dialogic",
"reason": "Tres sesiones con comprensión estancada.",
"confidence": 0.75, # opcional
}
Devuelve None (o {"recommended": False}) cuando no se justifica
ningún cambio.
on_session_complete¶
@hookspec
def on_session_complete(
session: dict,
rating: dict,
) -> None:
"""Hook de efecto secundario: disparado cuando se finaliza una sesión.
Implementado por: plugin de seguimiento (escribe un ProgressCommit).
Modo: list. Cada suscriptor se ejecuta; los errores se capturan y
se registran, pero no deshacen la finalización de la sesión.
"""
Los errores en este hook NO DEBEN propagarse: el envoltorio
_fire_on_session_complete en
backend/app/main.py los captura y registra.
get_progress_summary¶
@hookspec
def get_progress_summary(
project_id: str,
db: Session,
) -> dict | None:
"""Devuelve un segmento de espacio de nombres del resumen de progreso.
Implementado por: plugin de seguimiento (devuelve el segmento de
espacio de nombres "tracking").
Modo: list. Los segmentos se fusionan superficialmente en la ruta.
"""
Cada plugin devuelve un dict con clave de espacio de nombres único. El
plugin de seguimiento devuelve {"tracking": {...}, "step_evaluation": {...}}.
Un futuro plugin detector de estancamiento podría devolver
{"stagnation": {...}}, etc.
get_tool_recommendations¶
@hookspec
def get_tool_recommendations(
profile: dict,
lang: str,
limit: int = 5,
) -> list[dict] | None:
"""Devuelve recomendaciones de herramientas externas ordenadas.
Implementado por: plugin de herramientas.
Modo: list. La ruta actualmente usa el resultado del primer plugin;
un futuro recomendador multi-fuente podría fusionar.
"""
Forma del valor devuelto:
[
{
"name": "Anki",
"url": "https://apps.ankiweb.net/",
"why": "...",
"weight_keys": ["deductive", "error_based"],
"score": 0.5
},
...
]
firstresult vs modo list¶
| Hookspec | Modo | Por qué |
|---|---|---|
| get_assessment_questions | list (firstresult efectivo) | Actualmente un paquete; el futuro podría registrar paquetes nombrados |
| calculate_profile | firstresult | Exactamente un algoritmo |
| create_session_prompt | firstresult | Exactamente un compositor de prompts |
| ai_complete | firstresult | El proveedor correspondiente es el dueño de la llamada |
| recommend_method_switch | list | Múltiples plugins pueden sugerir |
| on_session_complete | list | Los efectos secundarios se dispersan |
| get_progress_summary | list | Los segmentos de espacio de nombres se fusionan |
| get_tool_recommendations | list (firstresult efectivo) | Actualmente una sola fuente |
Los modos list que actualmente se comportan como firstresult son
deliberados: el contrato está abierto a extensión multi-plugin, pero la
implementación de v0.7.0 usa solo el primero.